From 74088ec8e8b07c09784eab4610a39ef356af40a7 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Thu, 21 Mar 2024 20:21:53 -0700 Subject: [PATCH 001/358] Update CODEOWNERS --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e5f6f752..0e5b7f49 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @zkoppert +* @jmeridth @zkoppert From 4fdfe829c187e39cf70c0f3ea689197d0c2934c2 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Fri, 22 Mar 2024 07:00:50 -0500 Subject: [PATCH 002/358] feat: authenticate with github app instllation - [x] add ability to authenticate with GitHub App Installation - [x] add tests for GitHub App Installation - [x] add more tests for env vars repo updates to match standards: - [x] alphabetize :allthethings: - [x] add requirements-test.txt to version our testing dependencies - [x] update Makefile to point linting commands to our linter configs that the GitHub Action workflows use - [x] add linter configs - [x] add checkov exemptions to Dockerfile - custom user - can't do, need root for GitHub action workspace access - [x] update README, splitting authentication section - [x] change .pylintrc to .python-lint (pylint default config file) - [x] updated all examples to default to read contents permissions - [x] linted files I'd highly suggest this be a major version change as there are breaking changes: - [x] standardize boolean environment variables (true or false) - [x] drop GITHUB_SERVER_URL and instead use GITHUB_ENTERPRISE_URL for enterprise customers (matches our other repos) Signed-off-by: jmeridth --- .env-example | 15 +- .github/linters/.flake8 | 5 + .github/linters/.isort.cfg | 2 + .github/linters/.python-lint | 644 ++++++++++++++++++ .github/pull_request-template.md | 10 +- .github/workflows/linter.yaml | 35 + .pylintrc | 16 - Dockerfile | 2 + Makefile | 8 +- README.md | 50 +- config.py | 161 ++++- discussions.py | 1 + docs/assign-team-instead-of-individual.md | 6 +- ...enticating-with-github-app-installation.md | 62 ++ ...mple-using-json-instead-markdown-output.md | 6 +- docs/example-workflows.md | 18 +- docs/measure-time.md | 6 +- docs/search-query.md | 6 +- issue_metrics.py | 68 +- json_writer.py | 27 +- labels.py | 2 +- requirements-test.txt | 5 + test_config.py | 238 +++++++ test_discussions.py | 2 + test_issue_metrics.py | 55 +- test_json_writer.py | 1 + test_labels.py | 10 +- test_markdown_writer.py | 8 +- test_time_to_answer.py | 14 +- test_time_to_close.py | 7 +- test_time_to_first_response.py | 87 ++- test_time_to_merge.py | 3 +- test_time_to_ready_for_review.py | 3 +- time_to_answer.py | 2 +- time_to_close.py | 2 +- time_to_first_response.py | 2 +- time_to_merge.py | 1 + 37 files changed, 1395 insertions(+), 195 deletions(-) create mode 100644 .github/linters/.flake8 create mode 100644 .github/linters/.isort.cfg create mode 100644 .github/linters/.python-lint create mode 100644 .github/workflows/linter.yaml delete mode 100644 .pylintrc create mode 100644 docs/authenticating-with-github-app-installation.md create mode 100644 requirements-test.txt create mode 100644 test_config.py diff --git a/.env-example b/.env-example index 03429dd8..f50b62c4 100644 --- a/.env-example +++ b/.env-example @@ -1,4 +1,13 @@ -GH_TOKEN = " " -SEARCH_QUERY = "repo:owner/repo is:open is:issue" -LABELS_TO_MEASURE = "waiting-for-review,waiting-for-manager" +GH_APP_ID="" +GH_APP_INSTALLATION_ID="" +GH_APP_PRIVATE_KEY="" +GH_ENTERPRISE_URL = "" +GH_TOKEN = "" +HIDE_AUTHOR = "false" +HIDE_LABEL_METRICS = "false" +HIDE_TIME_TO_ANSWER = "false" +HIDE_TIME_TO_CLOSE = "false" +HIDE_TIME_TO_FIRST_RESPONSE = "false" IGNORE_USERS = "user1,user2" +LABELS_TO_MEASURE = "waiting-for-review,waiting-for-manager" +SEARCH_QUERY = "repo:owner/repo is:open is:issue" diff --git a/.github/linters/.flake8 b/.github/linters/.flake8 new file mode 100644 index 00000000..1aaf8292 --- /dev/null +++ b/.github/linters/.flake8 @@ -0,0 +1,5 @@ +[flake8] +exclude = venv,.venv,.git,__pycache__ +extend-ignore = C901 +max-line-length = 150 +statistics = True diff --git a/.github/linters/.isort.cfg b/.github/linters/.isort.cfg new file mode 100644 index 00000000..f238bf7e --- /dev/null +++ b/.github/linters/.isort.cfg @@ -0,0 +1,2 @@ +[settings] +profile = black diff --git a/.github/linters/.python-lint b/.github/linters/.python-lint new file mode 100644 index 00000000..da0f17df --- /dev/null +++ b/.github/linters/.python-lint @@ -0,0 +1,644 @@ +[MAIN] + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Clear in-memory caches upon conclusion of linting. Useful if running pylint +# in a server-like mode. +clear-cache-post-run=no + +# Load and enable all available extensions. Use --list-extensions to see a list +# all available extensions. +#enable-all-extensions= + +# In error mode, messages with a category besides ERROR or FATAL are +# suppressed, and no reports are done by default. Error mode is compatible with +# disabling specific errors. +#errors-only= + +# Always return a 0 (non-error) status code, even if lint errors are found. +# This is primarily useful in continuous integration scripts. +#exit-zero= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold under which the program will exit with error. +fail-under=10 + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +#from-stdin= + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\\' represents the directory delimiter on Windows systems, +# it can't be used as an escape character. +ignore-paths= + +# Files or directories matching the regular expression patterns are skipped. +# The regex matches against base names, not paths. The default value ignores +# Emacs file locks +ignore-patterns=^\.# + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.11 + +# Discover python modules and packages in the file system subtree. +recursive=no + +# Add paths to the list of the source roots. Supports globbing patterns. The +# source root is an absolute path or a path relative to the current working +# directory used to determine a package namespace for modules located under the +# source root. +source-roots= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# In verbose mode, extra non-checker-related info will be displayed. +#verbose= + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. If left empty, argument names will be checked with the set +# naming style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. If left empty, class names will be checked with the set naming style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. If left empty, function names will be checked with the set +# naming style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Regular expression matching correct type alias names. If left empty, type +# alias names will be checked with the set naming style. +#typealias-rgx= + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. If left empty, variable names will be checked with the set +# naming style. +#variable-rgx= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + asyncSetUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=builtins.BaseException,builtins.Exception + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow explicit reexports by alias from a package __init__. +allow-reexport-from-package=no + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence=HIGH, + CONTROL_FLOW, + INFERENCE, + INFERENCE_FAILURE, + UNDEFINED + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then re-enable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=bad-inline-option, + deprecated-pragma, + duplicate-code, + locally-disabled, + file-ignored, + import-error, + line-too-long, + raw-checker-failed, + suppressed-message, + too-few-public-methods, + too-many-arguments, + too-many-function-args, + too-many-branches, + too-many-locals, + too-many-statements, + useless-suppression, + use-symbolic-message-instead, + use-implicit-booleaness-not-comparison-to-string, + use-implicit-booleaness-not-comparison-to-zero, + wrong-import-order + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable= + + +[METHOD_ARGS] + +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +notes-rgx= + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each +# category, as well as 'statement' which is the total number of statements +# analyzed. This score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +msg-template= + +# Set the output format. Available formats are: text, parseable, colorized, +# json2 (improved json format), json (old json format) and msvs (visual +# studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +#output-format= + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=yes + +# Signatures are removed from the similarity computation +ignore-signatures=yes + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. No available dictionaries : You need to install +# both the python package and the system dependency for enchant to work. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins=no-member, + not-async-context-manager, + not-context-manager, + attribute-defined-outside-init + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io diff --git a/.github/pull_request-template.md b/.github/pull_request-template.md index 3df7a0b4..beb28c0a 100644 --- a/.github/pull_request-template.md +++ b/.github/pull_request-template.md @@ -1,17 +1,17 @@ - - - - +# Pull Request + ## Proposed Changes - + ## Readiness Checklist ### Author/Contributor + - [ ] If documentation is needed for this change, has that been included in this pull request - [ ] run `make lint` and fix any issues that you have introduced - [ ] run `make test` and ensure you have test coverage for the lines you are introducing ### Reviewer + - [ ] Label as either `bug`, `documentation`, `enhancement`, `infrastructure`, or `breaking` diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml new file mode 100644 index 00000000..280cb6a5 --- /dev/null +++ b/.github/workflows/linter.yaml @@ -0,0 +1,35 @@ +--- +name: Lint Code Base + +on: + pull_request: + branches: main + +permissions: + contents: read + +jobs: + lint: + name: Lint Code Base + runs-on: ubuntu-latest + permissions: + contents: read + packages: read + statuses: write + steps: + - name: Checkout Code + uses: actions/checkout@v4 + with: + # Full git history is needed to get a proper + # list of changed files within `super-linter` + fetch-depth: 0 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt -r requirements-test.txt + - name: Lint Code Base + uses: super-linter/super-linter@v6 + env: + DEFAULT_BRANCH: main + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_ACTIONS_COMMAND_ARGS: -shellcheck= diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index a6db1df5..00000000 --- a/.pylintrc +++ /dev/null @@ -1,16 +0,0 @@ -[MAIN] -ignore=.git, - __pycache__, - venv, - .venv, - - -[MESSAGES CONTROL] -disable= - redefined-argument-from-local, - too-many-arguments, - too-few-public-methods, - duplicate-code, - too-many-locals, - too-many-branches, - line-too-long, diff --git a/Dockerfile b/Dockerfile index c47ad10f..ad8dc999 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,5 @@ +#checkov:skip=CKV_DOCKER_2 +#checkov:skip=CKV_DOCKER_3 FROM python:3.12-slim LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ diff --git a/Makefile b/Makefile index 0437bed3..25a7781a 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,9 @@ clean: .PHONY: lint lint: # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude=venv,.venv,.git,__pycache__ + flake8 . --config=.github/linters/.flake8 --count --select=E9,F63,F7,F82 --show-source # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=15 --max-line-length=127 --statistics --exclude=venv,.venv,.git,__pycache__ - pylint --rcfile=.pylintrc --fail-under=9.0 *.py + flake8 . --config=.github/linters/.flake8 --count --exit-zero --max-complexity=15 --max-line-length=127 + isort --settings-file=.github/linters/.isort.cfg . + pylint --rcfile=.github/linters/.python-lint --fail-under=9.0 *.py + black . diff --git a/README.md b/README.md index 9a72180d..81310dc6 100644 --- a/README.md +++ b/README.md @@ -45,13 +45,15 @@ on: - cron: '3 2 1 * *' permissions: - issues: write - pull-requests: read + contents: read jobs: build: name: issue metrics runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: read steps: - name: Get dates for last month shell: bash @@ -106,17 +108,37 @@ If you need support using this project or have questions about it, please [open Below are the allowed configuration options: -| field | required | default | description | -|-----------------------|----------|---------|-------------| -| `GH_TOKEN` | True | | The GitHub Token used to scan the repository. Must have read access to all repository you are interested in scanning. | -| `SEARCH_QUERY` | True | | The query by which you can filter issues/prs which must contain a `repo:`, `org:`, `owner:`, or a `user:` entry. For discussions, include `type:discussions` in the query. | -| `LABELS_TO_MEASURE` | False | | A comma separated list of labels to measure how much time the label is applied. If not provided, no labels durations will be measured. Not compatible with discussions at this time. | -| `HIDE_AUTHOR` | False | | If set to any value, the author will not be displayed in the generated markdown file. | -| `HIDE_TIME_TO_FIRST_RESPONSE` | False | | If set to any value, the time to first response will not be displayed in the generated markdown file. | -| `HIDE_TIME_TO_CLOSE` | False | | If set to any value, the time to close will not be displayed in the generated markdown file. | -| `HIDE_TIME_TO_ANSWER` | False | | If set to any value, the time to answer a discussion will not be displayed in the generated markdown file. | -| `HIDE_LABEL_METRICS` | False | | If set to any value, the time in label metrics will not be displayed in the generated markdown file. | -| `IGNORE_USERS` | False | | A comma separated list of users to ignore when calculating metrics. (ie. `IGNORE_USERS: 'user1,user2'`). To ignore bots, append `[bot]` to the user (ie. `IGNORE_USERS: 'github-actions[bot]'`) | +#### Authentication + +This action can be configured to authenticate with GitHub App Installation or Personal Access Token (PAT). If all configuration options are provided, the GitHub App Installation configuration has precedence. You can choose one of the following methods to authenticate: + +##### GitHub App Installation + +| field | required | default | description | +|-------------------------------|----------|---------|-------------| +| `GH_APP_ID` | True | `""` | GitHub Application ID. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. | +| `GH_APP_INSTALLATION_ID` | True | `""` | GitHub Application Installation ID. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. | +| `GH_APP_PRIVATE_KEY` | True | `""` | GitHub Application Private Key. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. | + +##### Personal Access Token (PAT) + +| field | required | default | description | +|-------------------------------|----------|---------|-------------| +| `GH_TOKEN` | True | `""` | The GitHub Token used to scan the repository. Must have read access to all repository you are interested in scanning. | + +#### Other Configuration Options + +| field | required | default | description | +|-------------------------------|----------|---------|-------------| +| `GH_ENTERPRISE_URL` | False | `""` | URL of GitHub Enterprise instance to use for auth instead of github.com | +| `HIDE_AUTHOR` | False | False | If set to `true`, the author will not be displayed in the generated markdown file. | +| `HIDE_LABEL_METRICS` | False | False | If set to `true`, the time in label metrics will not be displayed in the generated markdown file. | +| `HIDE_TIME_TO_ANSWER` | False | False | If set to `true`, the time to answer a discussion will not be displayed in the generated markdown file. | +| `HIDE_TIME_TO_CLOSE` | False | False | If set to `true`, the time to close will not be displayed in the generated markdown file. | +| `HIDE_TIME_TO_FIRST_RESPONSE` | False | False | If set to `true`, the time to first response will not be displayed in the generated markdown file. | +| `IGNORE_USERS` | False | False | A comma separated list of users to ignore when calculating metrics. (ie. `IGNORE_USERS: 'user1,user2'`). To ignore bots, append `[bot]` to the user (ie. `IGNORE_USERS: 'github-actions[bot]'`) | +| `LABELS_TO_MEASURE` | False | `""` | A comma separated list of labels to measure how much time the label is applied. If not provided, no labels durations will be measured. Not compatible with discussions at this time. | +| `SEARCH_QUERY` | True | `""` | The query by which you can filter issues/PRs which must contain a `repo:`, `org:`, `owner:`, or a `user:` entry. For discussions, include `type:discussions` in the query. | ## Further Documentation @@ -126,8 +148,10 @@ Below are the allowed configuration options: - [Example using the JSON output instead of the markdown output](./docs/example-using-json-instead-markdown-output.md) - [Configuring the `SEARCH_QUERY`](./docs/search-query.md) - [Local usage without Docker](./docs/local-usage-without-docker.md) +- [Authenticating with GitHub App Installation](./docs/authenticating-with-github-app-installation.md) ## Contributions + We would ❤️ contributions to improve this action. Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for how to get involved. ## License diff --git a/config.py b/config.py index a1f05420..d7eb3833 100644 --- a/config.py +++ b/config.py @@ -8,9 +8,13 @@ Functions: get_env_vars: Retrieves and returns an instance of EnvVars populated with environment variables. """ + import os +from os.path import dirname, join from typing import List +from dotenv import load_dotenv + class EnvVars: # pylint: disable=too-many-instance-attributes @@ -18,45 +22,133 @@ class EnvVars: Environment variables Attributes: - search_query (str): Search query used to filter issues/prs/discussions on GitHub + gh_app_id (int | None): The GitHub App ID to use for authentication + gh_app_installation_id (int | None): The GitHub App Installation ID to use for authentication + gh_app_private_key_bytes (bytes): The GitHub App Private Key as bytes to use for authentication gh_token (str): GitHub personal access token (PAT) for API authentication - labels_to_measure (List[str]): List of labels to measure how much time the lable is applied + ghe (str): The GitHub Enterprise URL to use for authentication + hide_author (bool): If true, the author's information is hidden in the output + hide_label_metrics (bool): If true, the label metrics are hidden in the output + hide_time_to_answer (bool): If true, the time to answer discussions is hidden in the output + hide_time_to_close (bool): If true, the time to close metric is hidden in the output + hide_time_to_first_response (bool): If true, the time to first response metric is hidden in the output ignore_users (List[str]): List of usernames to ignore when calculating metrics - github_server_url (str): URL of GitHub server (Github.com or Github Enterprise) - hide_author (str): If set, the author's information is hidden in the output - hide_time_to_first_response (str): If set, the time to first response metric is hidden in the output - hide_time_to_close (str): If set, the time to close metric is hidden in the output - hide_time_to_answer (str): If set, the time to answer discussions is hidden in the output - hide_label_metrics (str): If set, the label metrics are hidden in the output + labels_to_measure (List[str]): List of labels to measure how much time the lable is applied + search_query (str): Search query used to filter issues/prs/discussions on GitHub """ - def __init__(self, search_query: str, gh_token: str, labels_to_measure: List[str], ignore_user: List[str], - github_server_url: str, hide_author: str, hide_time_to_first_response: str, - hide_time_to_close: str, hide_time_to_answer: str, hide_label_metrics: str): - self.search_query = search_query + + def __init__( + self, + gh_app_id: int, + gh_app_installation_id: int, + gh_app_private_key_bytes: bytes, + gh_token: str, + ghe: str, + hide_author: bool, + hide_label_metrics: bool, + hide_time_to_answer: bool, + hide_time_to_close: bool, + hide_time_to_first_response: bool, + ignore_user: List[str], + labels_to_measure: List[str], + search_query: str, + ): + self.gh_app_id = gh_app_id + self.gh_app_installation_id = gh_app_installation_id + self.gh_app_private_key_bytes = gh_app_private_key_bytes self.gh_token = gh_token - self.labels_to_measure = labels_to_measure + self.ghe = ghe self.ignore_users = ignore_user - self.github_server_url = github_server_url + self.labels_to_measure = labels_to_measure self.hide_author = hide_author - self.hide_time_to_first_response = hide_time_to_first_response - self.hide_time_to_close = hide_time_to_close - self.hide_time_to_answer = hide_time_to_answer self.hide_label_metrics = hide_label_metrics + self.hide_time_to_answer = hide_time_to_answer + self.hide_time_to_close = hide_time_to_close + self.hide_time_to_first_response = hide_time_to_first_response + self.search_query = search_query + def __repr__(self): + return ( + f"EnvVars(" + f"{self.gh_app_id}," + f"{self.gh_app_installation_id}," + f"{self.gh_app_private_key_bytes}," + f"{self.gh_token}," + f"{self.ghe}," + f"{self.hide_author}," + f"{self.hide_label_metrics}," + f"{self.hide_time_to_answer}," + f"{self.hide_time_to_close}," + f"{self.hide_time_to_first_response}," + f"{self.ignore_users}," + f"{self.labels_to_measure}," + f"{self.search_query})" + ) -def get_env_vars() -> EnvVars: + +def get_bool_env_var(env_var_name: str) -> bool: + """Get a boolean environment variable. + + Args: + env_var_name: The name of the environment variable to retrieve. + + Returns: + The value of the environment variable as a boolean. + """ + return os.environ.get(env_var_name, "").strip().lower() == "true" + + +def get_int_env_var(env_var_name: str) -> int | None: + """Get an integer environment variable. + + Args: + env_var_name: The name of the environment variable to retrieve. + + Returns: + The value of the environment variable as an integer or None. + """ + env_var = os.environ.get(env_var_name) + if env_var is None or not env_var.strip(): + return None + try: + return int(env_var) + except ValueError: + return None + + +def get_env_vars(test: bool = False) -> EnvVars: """ Get the environment variables for use in the script. Returns EnvVars object with all environment variables """ + if not test: + dotenv_path = join(dirname(__file__), ".env") + load_dotenv(dotenv_path) + search_query = os.getenv("SEARCH_QUERY") if not search_query: raise ValueError("SEARCH_QUERY environment variable not set") + gh_app_id = get_int_env_var("GH_APP_ID") + gh_app_private_key_bytes = os.environ.get("GH_APP_PRIVATE_KEY", "").encode("utf8") + gh_app_installation_id = get_int_env_var("GH_APP_INSTALLATION_ID") + + if gh_app_id and (not gh_app_private_key_bytes or not gh_app_installation_id): + raise ValueError( + "GH_APP_ID set and GH_APP_INSTALLATION_ID or GH_APP_PRIVATE_KEY variable not set" + ) + gh_token = os.getenv("GH_TOKEN") - if not gh_token: - raise ValueError("GITHUB_TOKEN environment variable not set") + if ( + not gh_app_id + and not gh_app_private_key_bytes + and not gh_app_installation_id + and not gh_token + ): + raise ValueError("GH_TOKEN environment variable not set") + + ghe = os.getenv("GH_ENTERPRISE_URL", default="").strip() labels_to_measure = os.getenv("LABELS_TO_MEASURE") if labels_to_measure: @@ -70,24 +162,25 @@ def get_env_vars() -> EnvVars: else: ignore_users = [] - github_server_url = os.getenv("GITHUB_SERVER_URL") - # Hidden columns - hide_author = os.getenv("HIDE_AUTHOR") - hide_time_to_first_response = os.getenv("HIDE_TIME_TO_FIRST_RESPONSE") - hide_time_to_close = os.getenv("HIDE_TIME_TO_CLOSE") - hide_time_to_answer = os.getenv("HIDE_TIME_TO_ANSWER") - hide_label_metrics = os.getenv("HIDE_LABEL_METRICS") + hide_author = get_bool_env_var("HIDE_AUTHOR") + hide_label_metrics = get_bool_env_var("HIDE_LABEL_METRICS") + hide_time_to_answer = get_bool_env_var("HIDE_TIME_TO_ANSWER") + hide_time_to_close = get_bool_env_var("HIDE_TIME_TO_CLOSE") + hide_time_to_first_response = get_bool_env_var("HIDE_TIME_TO_FIRST_RESPONSE") return EnvVars( - search_query, + gh_app_id, + gh_app_installation_id, + gh_app_private_key_bytes, gh_token, - labels_to_measure, - ignore_users, - github_server_url, + ghe, hide_author, - hide_time_to_first_response, - hide_time_to_close, + hide_label_metrics, hide_time_to_answer, - hide_label_metrics + hide_time_to_close, + hide_time_to_first_response, + ignore_users, + labels_to_measure, + search_query, ) diff --git a/discussions.py b/discussions.py index 28b03753..8cc3d99f 100644 --- a/discussions.py +++ b/discussions.py @@ -6,6 +6,7 @@ Get a list of discussions in a GitHub repository that match the search query. """ + import requests diff --git a/docs/assign-team-instead-of-individual.md b/docs/assign-team-instead-of-individual.md index 6b2b6e6c..99237f7b 100644 --- a/docs/assign-team-instead-of-individual.md +++ b/docs/assign-team-instead-of-individual.md @@ -14,13 +14,15 @@ on: - cron: '3 2 1 * *' permissions: - issues: write - pull-requests: read + contents: read jobs: build: name: issue metrics runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: read steps: diff --git a/docs/authenticating-with-github-app-installation.md b/docs/authenticating-with-github-app-installation.md new file mode 100644 index 00000000..ed62042b --- /dev/null +++ b/docs/authenticating-with-github-app-installation.md @@ -0,0 +1,62 @@ +# Authenticating with a GitHub App Installation + +Authenticating as an app installation lets your app access resources that are owned by the user or organization that installed the app. Authenticating as an app installation is ideal for automation workflows that don't involve user input. + +[Documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app#authentication-as-an-app-installation) for more details. + +```yaml +name: Monthly issue metrics +on: + workflow_dispatch: + schedule: + - cron: '3 2 1 * *' + +permissions: + contents: read + +jobs: + build: + name: issue metrics + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: read + + steps: + + - name: Get dates for last month + shell: bash + run: | + # Calculate the first day of the previous month + first_day=$(date -d "last month" +%Y-%m-01) + + # Calculate the last day of the previous month + last_day=$(date -d "$first_day +1 month -1 day" +%Y-%m-%d) + + #Set an environment variable with the date range + echo "$first_day..$last_day" + echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV" + + - name: Run issue-metrics tool + uses: github/issue-metrics@v2 + env: + GH_APP_ID: ${{ secrets.GITHUB_APP_ID }} + GH_APP_INSTALLATION_ID: ${{ secrets.GITHUB_APP_INSTALLATION_ID }} + GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }} + SEARCH_QUERY: 'repo:owner/repo is:issue created:${{ env.last_month }} -reason:"not planned"' + + - name: Get user names from team + run: | + teamMembers="$(gh api /orgs/ORG/teams/TEAM_SLUG/members | jq -r '.[].login' | paste -sd, -)" + echo 'TEAM_MEMBERS='$teamMembers >> $GITHUB_ENV + env: + GITHUB_TOKEN: ${{ secrets.CUSTOM_TOKEN }} + + - name: Create issue + uses: peter-evans/create-issue-from-file@v4 + with: + title: Monthly issue metrics report + token: ${{ secrets.GITHUB_TOKEN }} + content-filepath: ./issue_metrics.md + assignees: ${{ env.TEAM_MEMBERS }} +``` diff --git a/docs/example-using-json-instead-markdown-output.md b/docs/example-using-json-instead-markdown-output.md index 0396b54e..44f75816 100644 --- a/docs/example-using-json-instead-markdown-output.md +++ b/docs/example-using-json-instead-markdown-output.md @@ -10,13 +10,15 @@ on: - cron: '3 2 1 * *' permissions: - issues: write - pull-requests: read + contents: read jobs: build: name: issue metrics runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: read steps: - name: Run issue-metrics tool diff --git a/docs/example-workflows.md b/docs/example-workflows.md index b4fc8ff3..69656324 100644 --- a/docs/example-workflows.md +++ b/docs/example-workflows.md @@ -12,13 +12,15 @@ on: - cron: '3 2 1 * *' permissions: - issues: write - pull-requests: read + contents: read jobs: build: name: issue metrics runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: read steps: @@ -61,13 +63,15 @@ on: workflow_dispatch: permissions: - issues: write - pull-requests: read + contents: read jobs: build: name: issue metrics runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: read steps: @@ -97,13 +101,15 @@ on: workflow_dispatch: permissions: - issues: write - pull-requests: read + contents: read jobs: build: name: issue metrics runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: read steps: - name: Get dates for last month diff --git a/docs/measure-time.md b/docs/measure-time.md index 3547100a..cfa86e2c 100644 --- a/docs/measure-time.md +++ b/docs/measure-time.md @@ -12,13 +12,15 @@ on: workflow_dispatch: permissions: - issues: write - pull-requests: read + content: read jobs: build: name: issue metrics runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: read steps: diff --git a/docs/search-query.md b/docs/search-query.md index 13d03292..5154a834 100644 --- a/docs/search-query.md +++ b/docs/search-query.md @@ -49,13 +49,15 @@ on: - cron: '3 2 1 * *' permissions: - issues: write - pull-requests: read + contents: read jobs: build: name: issue metrics runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: read steps: diff --git a/issue_metrics.py b/issue_metrics.py index 033970e2..df911c2a 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -21,27 +21,26 @@ main(): Run the issue-metrics script. """ -from os.path import dirname, join import sys from typing import List, Union import github3 -from dotenv import load_dotenv - from classes import IssueWithMetrics +from config import get_env_vars from discussions import get_discussions from json_writer import write_to_json -from labels import get_stats_time_in_labels, get_label_metrics +from labels import get_label_metrics, get_stats_time_in_labels from markdown_writer import write_to_markdown from time_to_answer import get_stats_time_to_answer, measure_time_to_answer from time_to_close import get_stats_time_to_close, measure_time_to_close -from time_to_ready_for_review import get_time_to_ready_for_review -from time_to_merge import measure_time_to_merge from time_to_first_response import ( get_stats_time_to_first_response, measure_time_to_first_response, ) -from config import get_env_vars +from time_to_merge import measure_time_to_merge +from time_to_ready_for_review import get_time_to_ready_for_review + +GITHUB_BASE_URL = "https://github.com" def search_issues( @@ -90,23 +89,34 @@ def search_issues( return issues -def auth_to_github() -> github3.GitHub: +def auth_to_github( + gh_app_id: str, + gh_app_installation_id: int, + gh_app_private_key_bytes: bytes, + token: str, + ghe: str, +) -> github3.GitHub: """ Connect to GitHub.com or GitHub Enterprise, depending on env variables. Returns: github3.GitHub: A github api connection. """ - env_vars = get_env_vars() - token = env_vars.gh_token - github_server_url = env_vars.github_server_url - if github_server_url and github_server_url != "https://github.com": - github_connection = github3.GitHubEnterprise( - github_server_url, token=token + if gh_app_id and gh_app_private_key_bytes and gh_app_installation_id: + gh = github3.github.GitHub() + gh.login_as_app_installation( + gh_app_private_key_bytes, gh_app_id, gh_app_installation_id ) - else: + github_connection = gh + elif ghe and token: + github_connection = github3.github.GitHubEnterprise(ghe, token=token) + elif token: github_connection = github3.login(token=token) + else: + raise ValueError( + "GH_TOKEN or the set of [GH_APP_ID, GH_APP_INSTALLATION_ID, GH_APP_PRIVATE_KEY] environment variables are not set" + ) return github_connection # type: ignore @@ -182,9 +192,13 @@ def get_per_issue_metrics( issue_with_metrics.label_metrics = get_label_metrics(issue, labels) if issue.state == "closed": # type: ignore if pull_request: - issue_with_metrics.time_to_close = measure_time_to_merge(pull_request, ready_for_review_at) + issue_with_metrics.time_to_close = measure_time_to_merge( + pull_request, ready_for_review_at + ) else: - issue_with_metrics.time_to_close = measure_time_to_close(issue, None) + issue_with_metrics.time_to_close = measure_time_to_close( + issue, None + ) num_issues_closed += 1 elif issue.state == "open": # type: ignore num_issues_open += 1 @@ -231,19 +245,21 @@ def main(): print("Starting issue-metrics search...") - # Load env variables from file - dotenv_path = join(dirname(__file__), ".env") - load_dotenv(dotenv_path) - - # Auth to GitHub.com - github_connection = auth_to_github() - # Get the environment variables for use in the script env_vars = get_env_vars() search_query = env_vars.search_query token = env_vars.gh_token ignore_users = env_vars.ignore_users + # Auth to GitHub.com + github_connection = auth_to_github( + env_vars.gh_app_id, + env_vars.gh_app_installation_id, + env_vars.gh_app_private_key_bytes, + token, + env_vars.ghe, + ) + # Get the repository owner and name from the search query owner = get_owner(search_query) @@ -284,9 +300,7 @@ def main(): ignore_users=ignore_users, ) - stats_time_to_first_response = get_stats_time_to_first_response( - issues_with_metrics - ) + stats_time_to_first_response = get_stats_time_to_first_response(issues_with_metrics) stats_time_to_close = None if num_issues_closed > 0: stats_time_to_close = get_stats_time_to_close(issues_with_metrics) diff --git a/json_writer.py b/json_writer.py index cc4d12df..8f80e6f0 100644 --- a/json_writer.py +++ b/json_writer.py @@ -14,10 +14,9 @@ """ - import json -from datetime import timedelta import os +from datetime import timedelta from typing import List, Union from classes import IssueWithMetrics @@ -81,36 +80,36 @@ def write_to_json( med_time_to_first_response = None p90_time_to_first_response = None if stats_time_to_first_response is not None: - average_time_to_first_response = stats_time_to_first_response['avg'] - med_time_to_first_response = stats_time_to_first_response['med'] - p90_time_to_first_response = stats_time_to_first_response['90p'] + average_time_to_first_response = stats_time_to_first_response["avg"] + med_time_to_first_response = stats_time_to_first_response["med"] + p90_time_to_first_response = stats_time_to_first_response["90p"] # time to close average_time_to_close = None med_time_to_close = None p90_time_to_close = None if stats_time_to_close is not None: - average_time_to_close = stats_time_to_close['avg'] - med_time_to_close = stats_time_to_close['med'] - p90_time_to_close = stats_time_to_close['90p'] + average_time_to_close = stats_time_to_close["avg"] + med_time_to_close = stats_time_to_close["med"] + p90_time_to_close = stats_time_to_close["90p"] # time to answer average_time_to_answer = None med_time_to_answer = None p90_time_to_answer = None if stats_time_to_answer is not None: - average_time_to_answer = stats_time_to_answer['avg'] - med_time_to_answer = stats_time_to_answer['med'] - p90_time_to_answer = stats_time_to_answer['90p'] + average_time_to_answer = stats_time_to_answer["avg"] + med_time_to_answer = stats_time_to_answer["med"] + p90_time_to_answer = stats_time_to_answer["90p"] average_time_in_labels = {} med_time_in_labels = {} p90_time_in_labels = {} - for label, time in stats_time_in_labels['avg'].items(): + for label, time in stats_time_in_labels["avg"].items(): average_time_in_labels[label] = str(time) - for label, time in stats_time_in_labels['med'].items(): + for label, time in stats_time_in_labels["med"].items(): med_time_in_labels[label] = str(time) - for label, time in stats_time_in_labels['90p'].items(): + for label, time in stats_time_in_labels["90p"].items(): p90_time_in_labels[label] = str(time) # Create a dictionary with the metrics diff --git a/labels.py b/labels.py index 091a5a0f..fe1c8551 100644 --- a/labels.py +++ b/labels.py @@ -1,11 +1,11 @@ """ Functions for calculating time spent in labels. """ + from datetime import datetime, timedelta from typing import List import github3 import numpy import pytz - from classes import IssueWithMetrics diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 00000000..4b3c2dbd --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,5 @@ +black==24.3.0 +flake8==7.0.0 +pylint==3.1.0 +pytest==8.1.1 +pytest-cov==4.1.0 diff --git a/test_config.py b/test_config.py new file mode 100644 index 00000000..e4a00bb5 --- /dev/null +++ b/test_config.py @@ -0,0 +1,238 @@ +"""A module containing unit tests for the config module functions. + +Classes: + TestGetIntFromEnv: A class to test the get_int_env_var function. + TestEnvVars: A class to test the get_env_vars function. + +""" + +import os +import unittest +from unittest.mock import patch + +from config import EnvVars, get_env_vars, get_int_env_var + +SEARCH_QUERY = "is:issue is:open repo:user/repo" +TOKEN = "test_token" + + +class TestGetIntFromEnv(unittest.TestCase): + """ + Test suite for the get_int_from_env function. + + ... + + Test methods: + - test_get_int_env_var: Test returns the expected integer value. + - test_get_int_env_var_with_empty_env_var: Test returns None when environment variable + is empty. + - test_get_int_env_var_with_non_integer: Test returns None when environment variable + is a non-integer. + """ + + @patch.dict(os.environ, {"INT_ENV_VAR": "12345"}) + def test_get_int_env_var(self): + """ + Test that get_int_env_var returns the expected integer value. + """ + result = get_int_env_var("INT_ENV_VAR") + self.assertEqual(result, 12345) + + @patch.dict(os.environ, {"INT_ENV_VAR": ""}) + def test_get_int_env_var_with_empty_env_var(self): + """ + This test verifies that the get_int_env_var function returns None + when the environment variable is empty. + + """ + result = get_int_env_var("INT_ENV_VAR") + self.assertIsNone(result) + + @patch.dict(os.environ, {"INT_ENV_VAR": "not_an_int"}) + def test_get_int_env_var_with_non_integer(self): + """ + Test that get_int_env_var returns None when the environment variable is + a non-integer. + + """ + result = get_int_env_var("INT_ENV_VAR") + self.assertIsNone(result) + + +class TestGetEnvVars(unittest.TestCase): + """ + Test suite for the get_env_vars function. + """ + + def setUp(self): + env_keys = [ + "GH_APP_ID", + "GH_APP_INSTALLATION_ID", + "GH_APP_PRIVATE_KEY", + "GH_TOKEN", + "GHE", + "HIDE_AUTHOR", + "HIDE_LABEL_METRICS", + "HIDE_TIME_TO_ANSWER", + "HIDE_TIME_TO_CLOSE", + "HIDE_TIME_TO_FIRST_RESPONSE", + "IGNORE_USERS", + "LABELS_TO_MEASURE", + "SEARCH_QUERY", + ] + for key in env_keys: + if key in os.environ: + del os.environ[key] + + @patch.dict( + os.environ, + { + "GH_APP_ID": "12345", + "GH_APP_INSTALLATION_ID": "678910", + "GH_APP_PRIVATE_KEY": "hello", + "GH_TOKEN": "", + "GH_ENTERPRISE_URL": "", + "HIDE_AUTHOR": "", + "HIDE_LABEL_METRICS": "", + "HIDE_TIME_TO_ANSWER": "", + "HIDE_TIME_TO_CLOSE": "", + "HIDE_TIME_TO_FIRST_RESPONSE": "", + "IGNORE_USERS": "", + "LABELS_TO_MEASURE": "", + "SEARCH_QUERY": SEARCH_QUERY, + }, + clear=True, + ) + def test_get_env_vars_with_github_app(self): + """Test that all environment variables are set correctly using GitHub App""" + expected_result = EnvVars( + 12345, + 678910, + b"hello", + "", + "", + False, + False, + False, + False, + False, + [], + [], + SEARCH_QUERY, + ) + result = get_env_vars(True) + self.assertEqual(str(result), str(expected_result)) + + @patch.dict( + os.environ, + { + "GH_APP_ID": "", + "GH_APP_INSTALLATION_ID": "", + "GH_APP_PRIVATE_KEY": "", + "GH_ENTERPRISE_URL": "", + "GH_TOKEN": TOKEN, + "HIDE_AUTHOR": "", + "HIDE_LABEL_METRICS": "", + "HIDE_TIME_TO_ANSWER": "", + "HIDE_TIME_TO_CLOSE": "", + "HIDE_TIME_TO_FIRST_RESPONSE": "", + "IGNORE_USERS": "", + "LABELS_TO_MEASURE": "", + "SEARCH_QUERY": SEARCH_QUERY, + }, + clear=True, + ) + def test_get_env_vars_with_token(self): + """Test that all environment variables are set correctly using a list of repositories""" + expected_result = EnvVars( + None, + None, + b"", + TOKEN, + "", + False, + False, + False, + False, + False, + [], + [], + SEARCH_QUERY, + ) + result = get_env_vars(True) + self.assertEqual(str(result), str(expected_result)) + + @patch.dict( + os.environ, + { + "GH_APP_ID": "", + "GH_APP_INSTALLATION_ID": "", + "GH_APP_PRIVATE_KEY": "", + "GH_TOKEN": TOKEN, + "GH_ENTERPRISE_URL": "", + "HIDE_AUTHOR": "true", + "HIDE_LABEL_METRICS": "true", + "HIDE_TIME_TO_ANSWER": "true", + "HIDE_TIME_TO_CLOSE": "true", + "HIDE_TIME_TO_FIRST_RESPONSE": "true", + "IGNORE_USERS": "", + "LABELS_TO_MEASURE": "waiting-for-review,waiting-for-manager", + "SEARCH_QUERY": SEARCH_QUERY, + }, + ) + def test_get_env_vars_optional_values(self): + """Test that optional values are set to their default values if not provided""" + expected_result = EnvVars( + None, + None, + b"", + TOKEN, + "", + True, + True, + True, + True, + True, + [], + ["waiting-for-review", "waiting-for-manager"], + SEARCH_QUERY, + ) + result = get_env_vars(True) + self.assertEqual(str(result), str(expected_result)) + + @patch.dict( + os.environ, + { + "GH_APP_ID": "", + "GH_APP_INSTALLATION_ID": "", + "GH_APP_PRIVATE_KEY": "", + "GH_TOKEN": "", + "SEARCH_QUERY": SEARCH_QUERY, + }, + clear=True, + ) + def test_get_env_vars_missing_token(self): + """Test that an error is raised if the TOKEN environment variables is not set""" + with self.assertRaises(ValueError): + get_env_vars(True) + + @patch.dict( + os.environ, + { + "GH_APP_ID": "", + "GH_APP_INSTALLATION_ID": "", + "GH_APP_PRIVATE_KEY": "", + "GH_TOKEN": TOKEN, + "SEARCH_QUERY": "", + }, + clear=True, + ) + def test_get_env_vars_missing_query(self): + """Test that an error is raised if the SEARCH_QUERY environment variable is not set.""" + + with self.assertRaises(ValueError): + get_env_vars(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/test_discussions.py b/test_discussions.py index cc1d4606..ffea4356 100644 --- a/test_discussions.py +++ b/test_discussions.py @@ -4,8 +4,10 @@ TestGetDiscussions: A class to test the get_discussions function with mock GraphQL responses. """ + import unittest from unittest.mock import patch + from discussions import get_discussions diff --git a/test_issue_metrics.py b/test_issue_metrics.py index 71e49088..34b2e38a 100644 --- a/test_issue_metrics.py +++ b/test_issue_metrics.py @@ -12,11 +12,13 @@ TestMain: A class to test the main function. """ + import os import unittest from datetime import datetime, timedelta from unittest.mock import MagicMock, patch +import github3 import issue_metrics from issue_metrics import ( IssueWithMetrics, @@ -59,40 +61,49 @@ def test_search_issues(self): class TestAuthToGithub(unittest.TestCase): """Test the auth_to_github function.""" - @patch("github3.login") - @patch.dict(os.environ, {"GH_TOKEN": "test_token", "SEARCH_QUERY": "is:open repo:user/repo"}) - def test_auth_to_github_with_token(self, mock_login): - """Test that auth_to_github works with a token. + @patch("github3.github.GitHub.login_as_app_installation") + def test_auth_to_github_with_github_app(self, mock_login): + """ + Test the auth_to_github function when GitHub app + parameters provided. + """ + mock_login.return_value = MagicMock() + result = auth_to_github(12345, 678910, b"hello", "", "") - This test sets the GH_TOKEN environment variable and checks that - auth_to_github returns the expected GitHub connection. + self.assertIsInstance(result, github3.github.GitHub) + def test_auth_to_github_with_token(self): """ + Test the auth_to_github function when the token is provided. + """ + result = auth_to_github(None, None, b"", "token", "") - # Set up the mock GitHub connection - mock_gh = MagicMock() - mock_login.return_value = mock_gh - - # Call the function - github_connection = auth_to_github() + self.assertIsInstance(result, github3.github.GitHub) - # Check the results - self.assertEqual(github_connection, mock_gh) - mock_login.assert_called_once_with(token="test_token") + def test_auth_to_github_without_authentication_information(self): + """ + Test the auth_to_github function when authentication information is not provided. + Expect a ValueError to be raised. + """ + with self.assertRaises(ValueError): + auth_to_github(None, None, b"", "", "") - @patch.dict(os.environ, {}, clear=True) - def test_auth_to_github_no_token(self): - """Test that auth_to_github raises a ValueError if GH_TOKEN is not set.""" + def test_auth_to_github_with_ghe(self): + """ + Test the auth_to_github function when the GitHub Enterprise URL is provided. + """ + result = auth_to_github(None, None, b"", "token", "https://github.example.com") - # Call auth_to_github and check that it raises a ValueError - with self.assertRaises(ValueError): - issue_metrics.auth_to_github() + self.assertIsInstance(result, github3.github.GitHubEnterprise) class TestGetEnvVars(unittest.TestCase): """Test suite for the get_env_vars function.""" - @patch.dict(os.environ, {"GH_TOKEN": "test_token", "SEARCH_QUERY": "is:issue is:open repo:user/repo"}) + @patch.dict( + os.environ, + {"GH_TOKEN": "test_token", "SEARCH_QUERY": "is:issue is:open repo:user/repo"}, + ) def test_get_env_vars(self): """Test that the function correctly retrieves the environment variables.""" diff --git a/test_json_writer.py b/test_json_writer.py index d8070665..b7b18acd 100644 --- a/test_json_writer.py +++ b/test_json_writer.py @@ -3,6 +3,7 @@ import json import unittest from datetime import timedelta + from classes import IssueWithMetrics from json_writer import write_to_json diff --git a/test_labels.py b/test_labels.py index 35c07e0e..26f696f1 100644 --- a/test_labels.py +++ b/test_labels.py @@ -1,4 +1,5 @@ """ Unit tests for labels.py """ + import unittest from datetime import datetime, timedelta from unittest.mock import MagicMock @@ -6,8 +7,7 @@ import github3 import pytz from classes import IssueWithMetrics - -from labels import get_stats_time_in_labels, get_label_events, get_label_metrics +from labels import get_label_events, get_label_metrics, get_stats_time_in_labels class TestLabels(unittest.TestCase): @@ -84,9 +84,9 @@ def test_get_stats_time_in_labels(self): labels = ["bug", "feature"] metrics = get_stats_time_in_labels(self.issues_with_metrics, labels) print(metrics) - self.assertEqual(len(metrics['avg']), 2) - self.assertEqual(metrics['avg']["bug"], timedelta(days=2)) - self.assertIsNone(metrics['avg'].get("feature")) + self.assertEqual(len(metrics["avg"]), 2) + self.assertEqual(metrics["avg"]["bug"], timedelta(days=2)) + self.assertIsNone(metrics["avg"].get("feature")) if __name__ == "__main__": diff --git a/test_markdown_writer.py b/test_markdown_writer.py index 6988b943..00b585cc 100644 --- a/test_markdown_writer.py +++ b/test_markdown_writer.py @@ -6,6 +6,7 @@ environment variables set. """ + import os import unittest from datetime import timedelta @@ -17,10 +18,7 @@ @patch.dict( os.environ, - { - "SEARCH_QUERY": "is:open repo:user/repo", - "GH_TOKEN": "test_token" - }, + {"SEARCH_QUERY": "is:open repo:user/repo", "GH_TOKEN": "test_token"}, ) class TestWriteToMarkdown(unittest.TestCase): """Test the write_to_markdown function.""" @@ -249,7 +247,7 @@ def test_write_to_markdown_no_issues(self): "HIDE_TIME_TO_FIRST_RESPONSE": "True", "HIDE_TIME_TO_CLOSE": "True", "HIDE_TIME_TO_ANSWER": "True", - "HIDE_LABEL_METRICS": "True" + "HIDE_LABEL_METRICS": "True", }, ) class TestWriteToMarkdownWithEnv(unittest.TestCase): diff --git a/test_time_to_answer.py b/test_time_to_answer.py index 700079d1..5d5a44f9 100644 --- a/test_time_to_answer.py +++ b/test_time_to_answer.py @@ -53,13 +53,19 @@ def test_returns_stats_time_to_answer(self): # Arrange issues_with_metrics = [ - IssueWithMetrics("issue1", "url1", "alice", None, None, timedelta(seconds=10)), - IssueWithMetrics("issue2", "url2", "bob", None, None, timedelta(seconds=20)), - IssueWithMetrics("issue3", "url3", "carol", None, None, timedelta(seconds=30)), + IssueWithMetrics( + "issue1", "url1", "alice", None, None, timedelta(seconds=10) + ), + IssueWithMetrics( + "issue2", "url2", "bob", None, None, timedelta(seconds=20) + ), + IssueWithMetrics( + "issue3", "url3", "carol", None, None, timedelta(seconds=30) + ), ] # Act - result = get_stats_time_to_answer(issues_with_metrics)['avg'] + result = get_stats_time_to_answer(issues_with_metrics)["avg"] # Assert self.assertEqual(result, timedelta(seconds=20)) diff --git a/test_time_to_close.py b/test_time_to_close.py index c4596155..0c09b2cf 100644 --- a/test_time_to_close.py +++ b/test_time_to_close.py @@ -9,11 +9,12 @@ TestGetStatsTimeToClose: A class to test the get_stats_time_to_close function. """ -from datetime import timedelta + import unittest +from datetime import timedelta from unittest.mock import MagicMock -from classes import IssueWithMetrics +from classes import IssueWithMetrics from time_to_close import get_stats_time_to_close, measure_time_to_close @@ -44,7 +45,7 @@ def test_get_stats_time_to_close(self): ] # Call the function and check the result - result = get_stats_time_to_close(issues_with_metrics)['avg'] + result = get_stats_time_to_close(issues_with_metrics)["avg"] expected_result = timedelta(days=3) self.assertEqual(result, expected_result) diff --git a/test_time_to_first_response.py b/test_time_to_first_response.py index a1a124ad..437a0a9a 100644 --- a/test_time_to_first_response.py +++ b/test_time_to_first_response.py @@ -10,6 +10,7 @@ get_stats_time_to_first_response function. """ + import unittest from datetime import datetime, timedelta from unittest.mock import MagicMock @@ -77,14 +78,18 @@ def test_measure_time_to_first_response_with_pull_request_comments(self): # Set up the mock GitHub pull request comments mock_pr_comment1 = MagicMock() - mock_pr_comment1.submitted_at = datetime.fromisoformat("2023-01-02T00:00:00Z") # first response + mock_pr_comment1.submitted_at = datetime.fromisoformat( + "2023-01-02T00:00:00Z" + ) # first response mock_pr_comment2 = MagicMock() mock_pr_comment2.submitted_at = datetime.fromisoformat("2023-01-02T12:00:00Z") mock_pull_request = MagicMock() mock_pull_request.reviews.return_value = [mock_pr_comment1, mock_pr_comment2] # Call the function - result = measure_time_to_first_response(mock_issue1, None, mock_pull_request, None) + result = measure_time_to_first_response( + mock_issue1, None, mock_pull_request, None + ) expected_result = timedelta(days=1) # Check the results @@ -101,7 +106,9 @@ def test_measure_time_to_first_response_issue_comment_faster(self): # Set up the mock GitHub issue comment mock_comment1 = MagicMock() - mock_comment1.created_at = datetime.fromisoformat("2023-01-02T00:00:00Z") # first response + mock_comment1.created_at = datetime.fromisoformat( + "2023-01-02T00:00:00Z" + ) # first response mock_issue1.issue.comments.return_value = [mock_comment1] # Set up the mock GitHub pull request comment @@ -111,7 +118,9 @@ def test_measure_time_to_first_response_issue_comment_faster(self): mock_pull_request.reviews.return_value = [mock_pr_comment1] # Call the function - result = measure_time_to_first_response(mock_issue1, None, mock_pull_request, None) + result = measure_time_to_first_response( + mock_issue1, None, mock_pull_request, None + ) expected_result = timedelta(days=1) # Check the results @@ -133,18 +142,24 @@ def test_measure_time_to_first_response_pull_request_comment_faster(self): # Set up the mock GitHub pull request comment mock_pr_comment1 = MagicMock() - mock_pr_comment1.submitted_at = datetime.fromisoformat("2023-01-02T00:00:00Z") # first response + mock_pr_comment1.submitted_at = datetime.fromisoformat( + "2023-01-02T00:00:00Z" + ) # first response mock_pull_request = MagicMock() mock_pull_request.reviews.return_value = [mock_pr_comment1] # Call the function - result = measure_time_to_first_response(mock_issue1, None, mock_pull_request, None) + result = measure_time_to_first_response( + mock_issue1, None, mock_pull_request, None + ) expected_result = timedelta(days=1) # Check the results self.assertEqual(result, expected_result) - def test_measure_time_to_first_response_pull_request_comment_ignore_before_ready(self): + def test_measure_time_to_first_response_pull_request_comment_ignore_before_ready( + self, + ): """Test that measure_time_to_first_response ignores comments from before the pull request was ready for review.""" # Set up the mock GitHub issues mock_issue1 = MagicMock() @@ -163,14 +178,18 @@ def test_measure_time_to_first_response_pull_request_comment_ignore_before_ready mock_pr_comment1 = MagicMock() mock_pr_comment1.submitted_at = datetime.fromisoformat("2023-01-02T12:00:00Z") mock_pr_comment2 = MagicMock() - mock_pr_comment2.submitted_at = datetime.fromisoformat("2023-01-04T00:00:00Z") # first response + mock_pr_comment2.submitted_at = datetime.fromisoformat( + "2023-01-04T00:00:00Z" + ) # first response mock_pull_request = MagicMock() mock_pull_request.reviews.return_value = [mock_pr_comment1, mock_pr_comment2] ready_for_review_at = datetime.fromisoformat("2023-01-03T00:00:00Z") # Call the function - result = measure_time_to_first_response(mock_issue1, None, mock_pull_request, ready_for_review_at) + result = measure_time_to_first_response( + mock_issue1, None, mock_pull_request, ready_for_review_at + ) expected_result = timedelta(days=1) # Check the results @@ -199,12 +218,16 @@ def test_measure_time_to_first_response_ignore_users(self): mock_pr_comment1.submitted_at = datetime.fromisoformat("2023-01-03T00:00:00Z") mock_pr_comment2 = MagicMock() mock_pr_comment2.user.login = "not_ignored_user" - mock_pr_comment2.submitted_at = datetime.fromisoformat("2023-01-04T00:00:00Z") # first response + mock_pr_comment2.submitted_at = datetime.fromisoformat( + "2023-01-04T00:00:00Z" + ) # first response mock_pull_request = MagicMock() mock_pull_request.reviews.return_value = [mock_pr_comment1, mock_pr_comment2] # Call the function - result = measure_time_to_first_response(mock_issue1, None, mock_pull_request, None, ["ignored_user"]) + result = measure_time_to_first_response( + mock_issue1, None, mock_pull_request, None, ["ignored_user"] + ) expected_result = timedelta(days=3) # Check the results @@ -239,7 +262,11 @@ def test_measure_time_to_first_response_only_ignored_users(self): # Call the function result = measure_time_to_first_response( - mock_issue1, None, mock_pull_request, None, ["ignored_user", "ignored_user2"] + mock_issue1, + None, + mock_pull_request, + None, + ["ignored_user", "ignored_user2"], ) expected_result = None @@ -274,7 +301,9 @@ def test_measure_time_to_first_response_ignore_issue_owners_comment(self): mock_pull_request.reviews.return_value = [mock_pr_comment1, mock_pr_comment2] # Call the function - result = measure_time_to_first_response(mock_issue1, None, mock_pull_request, None) + result = measure_time_to_first_response( + mock_issue1, None, mock_pull_request, None + ) expected_result = timedelta(days=3) # Check the results @@ -299,12 +328,16 @@ def test_measure_time_to_first_response_ignore_bot(self): mock_pr_comment1.submitted_at = datetime.fromisoformat("2023-01-03T00:00:00Z") mock_pr_comment2 = MagicMock() mock_pr_comment2.user.type = "User" - mock_pr_comment2.submitted_at = datetime.fromisoformat("2023-01-04T00:00:00Z") # first response + mock_pr_comment2.submitted_at = datetime.fromisoformat( + "2023-01-04T00:00:00Z" + ) # first response mock_pull_request = MagicMock() mock_pull_request.reviews.return_value = [mock_pr_comment1, mock_pr_comment2] # Call the function - result = measure_time_to_first_response(mock_issue1, None, mock_pull_request, None) + result = measure_time_to_first_response( + mock_issue1, None, mock_pull_request, None + ) expected_result = timedelta(days=3) # Check the results @@ -325,16 +358,24 @@ def test_get_stats_time_to_first_response(self): # Create mock data issues_with_metrics = [ IssueWithMetrics( - "Issue 1", "https://github.com/user/repo/issues/1", "alice", timedelta(days=1) + "Issue 1", + "https://github.com/user/repo/issues/1", + "alice", + timedelta(days=1), + ), + IssueWithMetrics( + "Issue 2", + "https://github.com/user/repo/issues/2", + "bob", + timedelta(days=2), ), IssueWithMetrics( - "Issue 2", "https://github.com/user/repo/issues/2", "bob", timedelta(days=2) + "Issue 3", "https://github.com/user/repo/issues/3", "carol", None ), - IssueWithMetrics("Issue 3", "https://github.com/user/repo/issues/3", "carol", None), ] # Call the function and check the result - result = get_stats_time_to_first_response(issues_with_metrics)['avg'] + result = get_stats_time_to_first_response(issues_with_metrics)["avg"] expected_result = timedelta(days=1.5) self.assertEqual(result, expected_result) @@ -343,8 +384,12 @@ def test_get_stats_time_to_first_response_with_all_none(self): # Create mock data with all None issues_with_metrics = [ - IssueWithMetrics("Issue 1", "https://github.com/user/repo/issues/1", "alice", None), - IssueWithMetrics("Issue 2", "https://github.com/user/repo/issues/2", "bob", None), + IssueWithMetrics( + "Issue 1", "https://github.com/user/repo/issues/1", "alice", None + ), + IssueWithMetrics( + "Issue 2", "https://github.com/user/repo/issues/2", "bob", None + ), ] # Call the function and check the result diff --git a/test_time_to_merge.py b/test_time_to_merge.py index 08fa4966..a7af2aa4 100644 --- a/test_time_to_merge.py +++ b/test_time_to_merge.py @@ -8,8 +8,9 @@ TestMeasureTimeToMerge: A class to test the measure_time_to_merge function. """ -from datetime import timedelta, datetime + import unittest +from datetime import datetime, timedelta from unittest.mock import MagicMock from time_to_merge import measure_time_to_merge diff --git a/test_time_to_ready_for_review.py b/test_time_to_ready_for_review.py index cd2b6267..ff615a33 100644 --- a/test_time_to_ready_for_review.py +++ b/test_time_to_ready_for_review.py @@ -8,8 +8,9 @@ TestGetTimeToReadyForReview: A class to test the get_time_to_ready_for_review function. """ -from datetime import datetime + import unittest +from datetime import datetime from unittest.mock import MagicMock from time_to_ready_for_review import get_time_to_ready_for_review diff --git a/time_to_answer.py b/time_to_answer.py index 90d1769b..9e330a51 100644 --- a/time_to_answer.py +++ b/time_to_answer.py @@ -14,11 +14,11 @@ Measure the time it takes to answer a GitHub discussion. """ + from datetime import datetime, timedelta from typing import List, Union import numpy - from classes import IssueWithMetrics diff --git a/time_to_close.py b/time_to_close.py index 7791ed68..6887aa15 100644 --- a/time_to_close.py +++ b/time_to_close.py @@ -15,12 +15,12 @@ Calculate stats describing the time to close for a list of issues with metrics. """ + from datetime import datetime, timedelta from typing import List, Union import github3 import numpy - from classes import IssueWithMetrics diff --git a/time_to_first_response.py b/time_to_first_response.py index 3193fa69..e3427edb 100644 --- a/time_to_first_response.py +++ b/time_to_first_response.py @@ -17,12 +17,12 @@ Calculate stats describing time to first response for a list of issues with metrics. """ + from datetime import datetime, timedelta from typing import List, Union import github3 import numpy - from classes import IssueWithMetrics diff --git a/time_to_merge.py b/time_to_merge.py index dbd4f2c4..b9587319 100644 --- a/time_to_merge.py +++ b/time_to_merge.py @@ -11,6 +11,7 @@ Measure the time it takes to merge a pull request. """ + from datetime import datetime, timedelta from typing import Union From 92ced176555b1aacf5aaf771e0c99ad54bd51c58 Mon Sep 17 00:00:00 2001 From: Jason Meridth Date: Fri, 22 Mar 2024 18:29:59 -0500 Subject: [PATCH 003/358] Update README.md Co-authored-by: Zack Koppert --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 81310dc6..745d70d4 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ jobs: runs-on: ubuntu-latest permissions: issues: write - pull-requests: read + pull-requests: read steps: - name: Get dates for last month shell: bash From 59a25347f4f3e55eb956e4134405550859a3d13a Mon Sep 17 00:00:00 2001 From: jmeridth Date: Fri, 22 Mar 2024 18:33:02 -0500 Subject: [PATCH 004/358] fix: linting fixes more to come Signed-off-by: jmeridth --- .devcontainer/devcontainer.json | 14 ----- .github/workflows/codeql-analysis.yml | 3 + .github/workflows/docker-image.yml | 6 +- .github/workflows/major-version-updater.yml | 59 ++++++++++--------- .github/workflows/python-package.yml | 8 ++- .github/workflows/release-drafter.yml | 6 ++ Dockerfile | 1 - README.md | 16 ++--- docs/assign-team-instead-of-individual.md | 8 ++- ...mple-using-json-instead-markdown-output.md | 4 +- docs/search-query.md | 4 +- 11 files changed, 69 insertions(+), 60 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9dae20c9..0d380a23 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,27 +1,13 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile { "name": "Existing Dockerfile", "build": { - // Sets the run context to one level up instead of the .devcontainer folder. "context": "..", "dockerfile": "../Dockerfile" }, - // Features to add to the dev container. More info: https://containers.dev/features. "features": { "ghcr.io/devcontainers/features/common-utils:2": {} }, - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - - // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "echo hello", - - // Configure tool-specific properties. - // "customizations": {}, - - // Connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root. "remoteUser": "devcontainer" } diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 18dc5d2f..26d0c92c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -21,6 +21,9 @@ on: schedule: - cron: '27 19 * * 5' +permissions: + contents: read + jobs: analyze: name: Analyze diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 22604a23..ba0371a0 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -7,12 +7,12 @@ on: pull_request: branches: [ main ] -jobs: +permissions: + contents: read +jobs: build: - runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 - name: Build the Docker image diff --git a/.github/workflows/major-version-updater.yml b/.github/workflows/major-version-updater.yml index 4ace19e0..70af85df 100644 --- a/.github/workflows/major-version-updater.yml +++ b/.github/workflows/major-version-updater.yml @@ -1,28 +1,33 @@ --- - name: Major Version Updater - # Whenever a new release is made, push a major version tag - on: - release: - types: [ published ] - - jobs: - update-major-version-tag: - runs-on: ubuntu-latest - steps: - - name: Checkout Repo - uses: actions/checkout@v4 - - - name: version - id: version - run: | - tag=${GITHUB_REF/refs\/tags\//} - version=${tag#v} - major=${version%%.*} - echo "tag=${tag}" >> "$GITHUB_OUTPUT" - echo "version=${version}" >> "$GITHUB_OUTPUT" - echo "major=${major}" >> "$GITHUB_OUTPUT" - - - name: force update major tag - run: | - git tag v${{ steps.version.outputs.major }} - git push origin refs/tags/v${{ steps.version.outputs.major }} -f \ No newline at end of file +name: Major Version Updater +# Whenever a new release is made, push a major version tag +on: + release: + types: [ published ] + +permissions: + contents: write + +jobs: + update-major-version-tag: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + - name: version + id: version + run: | + tag=${GITHUB_REF/refs\/tags\//} + version=${tag#v} + major=${version%%.*} + echo "tag=${tag}" >> "$GITHUB_OUTPUT" + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "major=${major}" >> "$GITHUB_OUTPUT" + + - name: force update major tag + run: | + git tag v${{ steps.version.outputs.major }} + git push origin refs/tags/v${{ steps.version.outputs.major }} -f diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 43c3727e..3cd4d7fc 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -10,13 +10,16 @@ on: pull_request: branches: [ main ] +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.11, 3.12 ] + python-version: [3.11, 3.12] steps: - uses: actions/checkout@v4 @@ -27,8 +30,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install flake8 pylint pytest pytest-cov - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install -r requirements.txt -r requirements-test.txt - name: Lint with flake8 and pylint run: | make lint diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 14e0b9b5..c987bebb 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -7,9 +7,15 @@ on: branches: - main +permissions: + contents: read + jobs: update_release_draft: runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write steps: - uses: release-drafter/release-drafter@v6 env: diff --git a/Dockerfile b/Dockerfile index ad8dc999..9620d364 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,6 @@ COPY requirements.txt *.py /action/workspace/ RUN python3 -m pip install --no-cache-dir -r requirements.txt \ && apt-get -y update \ && apt-get -y install --no-install-recommends git-all=1:2.39.2-1.1 \ - && apt-get install build-essential -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* CMD ["/action/workspace/issue_metrics.py"] diff --git a/README.md b/README.md index 745d70d4..7f7ceb08 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ If you need support using this project or have questions about it, please [open 2. Select a best fit workflow file from the [examples directory](./docs/example-workflows.md) for your use case. 3. Copy that example into your repository (from step 1) and into the proper directory for GitHub Actions: `.github/workflows/` directory with the file extension `.yml` (ie. `.github/workflows/issue-metrics.yml`) 4. Edit the values (`SEARCH_QUERY`, `assignees`) from the sample workflow with your information. See the [SEARCH_QUERY](./docs/search-query.md) section for more information on how to configure the search query. -5. If you are running metrics on a repository other than the one where the workflow file is going to be, then update the value of `GH_TOKEN`. Do this by creating a [GitHub API token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic) with permissions to read the repo and write issues. Then take the value of the API token you just created, and [create a repository secret](https://docs.github.com/en/actions/security-guides/encrypted-secrets) where the name of the secret is `GH_TOKEN` and the value of the secret the API token. Then finally update the workflow file to use that repository secret by changing `GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}` to `GH_TOKEN: ${{ secrets.GH_TOKEN }}`. The name of the secret can really be anything. It just needs to match between when you create the secret name and when you refer to it in the workflow file. +5. If you are running metrics on a repository other than the one where the workflow file is going to be, then update the value of `GH_TOKEN`. Do this by creating a [GitHub API token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic) with permissions to read the repository and write issues. Then take the value of the API token you just created, and [create a repository secret](https://docs.github.com/en/actions/security-guides/encrypted-secrets) where the name of the secret is `GH_TOKEN` and the value of the secret the API token. Then finally update the workflow file to use that repository secret by changing `GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}` to `GH_TOKEN: ${{ secrets.GH_TOKEN }}`. The name of the secret can really be anything. It just needs to match between when you create the secret name and when you refer to it in the workflow file. 6. If you want the resulting issue with the metrics in it to appear in a different repository other than the one the workflow file runs in, update the line `token: ${{ secrets.GITHUB_TOKEN }}` with your own GitHub API token stored as a repository secret. This process is the same as described in the step above. More info on creating secrets can be found [here](https://docs.github.com/en/actions/security-guides/encrypted-secrets). 7. Commit the workflow file to the default branch (often `master` or `main`) 8. Wait for the action to trigger based on the `schedule` entry or manually trigger the workflow as shown in the [documentation](https://docs.github.com/en/actions/using-workflows/manually-running-a-workflow). @@ -131,11 +131,11 @@ This action can be configured to authenticate with GitHub App Installation or Pe | field | required | default | description | |-------------------------------|----------|---------|-------------| | `GH_ENTERPRISE_URL` | False | `""` | URL of GitHub Enterprise instance to use for auth instead of github.com | -| `HIDE_AUTHOR` | False | False | If set to `true`, the author will not be displayed in the generated markdown file. | -| `HIDE_LABEL_METRICS` | False | False | If set to `true`, the time in label metrics will not be displayed in the generated markdown file. | -| `HIDE_TIME_TO_ANSWER` | False | False | If set to `true`, the time to answer a discussion will not be displayed in the generated markdown file. | -| `HIDE_TIME_TO_CLOSE` | False | False | If set to `true`, the time to close will not be displayed in the generated markdown file. | -| `HIDE_TIME_TO_FIRST_RESPONSE` | False | False | If set to `true`, the time to first response will not be displayed in the generated markdown file. | +| `HIDE_AUTHOR` | False | False | If set to `true`, the author will not be displayed in the generated Markdown file. | +| `HIDE_LABEL_METRICS` | False | False | If set to `true`, the time in label metrics will not be displayed in the generated Markdown file. | +| `HIDE_TIME_TO_ANSWER` | False | False | If set to `true`, the time to answer a discussion will not be displayed in the generated Markdown file. | +| `HIDE_TIME_TO_CLOSE` | False | False | If set to `true`, the time to close will not be displayed in the generated Markdown file. | +| `HIDE_TIME_TO_FIRST_RESPONSE` | False | False | If set to `true`, the time to first response will not be displayed in the generated Markdown file. | | `IGNORE_USERS` | False | False | A comma separated list of users to ignore when calculating metrics. (ie. `IGNORE_USERS: 'user1,user2'`). To ignore bots, append `[bot]` to the user (ie. `IGNORE_USERS: 'github-actions[bot]'`) | | `LABELS_TO_MEASURE` | False | `""` | A comma separated list of labels to measure how much time the label is applied. If not provided, no labels durations will be measured. Not compatible with discussions at this time. | | `SEARCH_QUERY` | True | `""` | The query by which you can filter issues/PRs which must contain a `repo:`, `org:`, `owner:`, or a `user:` entry. For discussions, include `type:discussions` in the query. | @@ -145,7 +145,7 @@ This action can be configured to authenticate with GitHub App Installation or Pe - [Example workflows](./docs/example-workflows.md) - [Measuring time spent in labels](./docs/measure-time.md) - [Assigning teams instead of individuals](./docs/assign-team-instead-of-individual.md) -- [Example using the JSON output instead of the markdown output](./docs/example-using-json-instead-markdown-output.md) +- [Example using the JSON output instead of the Markdown output](./docs/example-using-json-instead-markdown-output.md) - [Configuring the `SEARCH_QUERY`](./docs/search-query.md) - [Local usage without Docker](./docs/local-usage-without-docker.md) - [Authenticating with GitHub App Installation](./docs/authenticating-with-github-app-installation.md) @@ -160,4 +160,4 @@ We would ❤️ contributions to improve this action. Please see [CONTRIBUTING.m ## More OSPO Tools -Looking for more resources for your open source program office (OSPO)? Check out the [`github-ospo`](https://github.com/github/github-ospo) repo for a variety of tools designed to support your needs. +Looking for more resources for your open source program office (OSPO)? Check out the [`github-ospo`](https://github.com/github/github-ospo) repoistory for a variety of tools designed to support your needs. diff --git a/docs/assign-team-instead-of-individual.md b/docs/assign-team-instead-of-individual.md index 99237f7b..9581a132 100644 --- a/docs/assign-team-instead-of-individual.md +++ b/docs/assign-team-instead-of-individual.md @@ -2,7 +2,13 @@ The assignee part of this workflow action comes from [a different GitHub action](https://github.com/peter-evans/create-issue-from-file) and currently GitHub issues don't support assigning groups. -By way of work around, you could use the [GitHub API to retrieve the members of the team](https://docs.github.com/en/rest/teams/members?apiVersion=2022-11-28#list-team-members) and then put them in a comma separated string that you provide as the assignee. This requires setting up a new GitHub API token (referred to below as `CUSTOM_TOKEN`) which has `read:org` permissions assigned and single sign on authorization as needed. To do this, create a [GitHub API token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic) with permissions to read the org (`read:org`). Then take the value of the API token you just created, and [create a repository secret](https://docs.github.com/en/actions/security-guides/encrypted-secrets) where the name of the secret is `CUSTOM_TOKEN` and the value of the secret the API token. +By way of work around, you could use the [GitHub API to retrieve the members of the team](https://docs.github.com/en/rest/teams/members?apiVersion=2022-11-28#list-team-members) and then put them in a comma separated string that you provide as the assignee. + +This requires setting up a new GitHub API token (referred to below as `CUSTOM_TOKEN`) which has `read:org` permissions assigned and single sign on authorization as needed. + +To do this, create a [GitHub API token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic) with permissions to read the org (`read:org`). + +Then take the value of the API token you just created, and [create a repository secret](https://docs.github.com/en/actions/security-guides/encrypted-secrets) where the name of the secret is `CUSTOM_TOKEN` and the value of the secret the API token. That might look something like the workflow below where `ORG` is your organization name and `TEAM_SLUG` is the name of the team: diff --git a/docs/example-using-json-instead-markdown-output.md b/docs/example-using-json-instead-markdown-output.md index 44f75816..7d82fcd9 100644 --- a/docs/example-using-json-instead-markdown-output.md +++ b/docs/example-using-json-instead-markdown-output.md @@ -1,6 +1,6 @@ -# Example using the JSON output instead of the markdown output +# Example using the JSON output instead of the Markdown output -There is JSON output available as well. You could use it for any number of possibilities, but here is one example that demonstrates retreiving the JSON output and then printing it out. +There is JSON output available as well. You could use it for any number of possibilities, but here is one example that demonstrates retrieving the JSON output and then printing it out. ```yaml name: Monthly issue metrics diff --git a/docs/search-query.md b/docs/search-query.md index 5154a834..61c5c893 100644 --- a/docs/search-query.md +++ b/docs/search-query.md @@ -1,7 +1,9 @@ # Configuring the `SEARCH_QUERY` Issues or Pull Requests? Open or closed? -This action can be configured to run metrics on discussions, pull requests and/or issues. It is also configurable by whether they were open or closed in the specified time window. Further query options are listed in the documentation on [searching issues and pull requests](https://docs.github.com/en/issues/tracking-your-work-with-issues/filtering-and-searching-issues-and-pull-requests) or [searching discussions](https://docs.github.com/en/search-github/searching-on-github/searching-discussions). Search results are limited to 1000 results by the GitHub API. +This action can be configured to run metrics on discussions, pull requests and/or issues. It is also configurable by whether they were open or closed in the specified time window. + +Further query options are listed in the documentation on [searching issues and pull requests](https://docs.github.com/en/issues/tracking-your-work-with-issues/filtering-and-searching-issues-and-pull-requests) or [searching discussions](https://docs.github.com/en/search-github/searching-on-github/searching-discussions). Search results are limited to 1000 results by the GitHub API. ## Examples From 3a603c7b102ef11458cf2d5e37206730c056d151 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Sat, 23 Mar 2024 23:44:31 -0500 Subject: [PATCH 005/358] chore: more linting (mypy) fixes. more to come Signed-off-by: jmeridth --- .github/linters/.mypy.ini | 5 +++++ Makefile | 1 + config.py | 28 +++++++++++++--------------- issue_metrics.py | 2 +- labels.py | 2 +- requirements-test.txt | 4 ++++ time_to_first_response.py | 4 ++-- 7 files changed, 27 insertions(+), 19 deletions(-) create mode 100644 .github/linters/.mypy.ini diff --git a/.github/linters/.mypy.ini b/.github/linters/.mypy.ini new file mode 100644 index 00000000..f0d4703d --- /dev/null +++ b/.github/linters/.mypy.ini @@ -0,0 +1,5 @@ +[mypy] +disable_error_code = attr-defined, import-not-found + +[mypy-github3.*] +ignore_missing_imports = True diff --git a/Makefile b/Makefile index 25a7781a..58d74793 100644 --- a/Makefile +++ b/Makefile @@ -14,4 +14,5 @@ lint: flake8 . --config=.github/linters/.flake8 --count --exit-zero --max-complexity=15 --max-line-length=127 isort --settings-file=.github/linters/.isort.cfg . pylint --rcfile=.github/linters/.python-lint --fail-under=9.0 *.py + mypy --config-file=.github/linters/.mypy.ini *.py black . diff --git a/config.py b/config.py index d7eb3833..e7d87be2 100644 --- a/config.py +++ b/config.py @@ -25,7 +25,7 @@ class EnvVars: gh_app_id (int | None): The GitHub App ID to use for authentication gh_app_installation_id (int | None): The GitHub App Installation ID to use for authentication gh_app_private_key_bytes (bytes): The GitHub App Private Key as bytes to use for authentication - gh_token (str): GitHub personal access token (PAT) for API authentication + gh_token (str | None): GitHub personal access token (PAT) for API authentication ghe (str): The GitHub Enterprise URL to use for authentication hide_author (bool): If true, the author's information is hidden in the output hide_label_metrics (bool): If true, the label metrics are hidden in the output @@ -39,11 +39,11 @@ class EnvVars: def __init__( self, - gh_app_id: int, - gh_app_installation_id: int, + gh_app_id: int | None, + gh_app_installation_id: int | None, gh_app_private_key_bytes: bytes, - gh_token: str, - ghe: str, + gh_token: str | None, + ghe: str | None, hide_author: bool, hide_label_metrics: bool, hide_time_to_answer: bool, @@ -150,17 +150,15 @@ def get_env_vars(test: bool = False) -> EnvVars: ghe = os.getenv("GH_ENTERPRISE_URL", default="").strip() - labels_to_measure = os.getenv("LABELS_TO_MEASURE") + labels_to_measure_list: List[str] = [] + labels_to_measure: str | None = os.getenv("LABELS_TO_MEASURE") if labels_to_measure: - labels_to_measure = labels_to_measure.split(",") - else: - labels_to_measure = [] + labels_to_measure_list = labels_to_measure.split(",") - ignore_users = os.getenv("IGNORE_USERS") + ignore_users_list: List[str] = [] + ignore_users: str | None = os.getenv("IGNORE_USERS") if ignore_users: - ignore_users = ignore_users.split(",") - else: - ignore_users = [] + ignore_users_list = ignore_users.split(",") # Hidden columns hide_author = get_bool_env_var("HIDE_AUTHOR") @@ -180,7 +178,7 @@ def get_env_vars(test: bool = False) -> EnvVars: hide_time_to_answer, hide_time_to_close, hide_time_to_first_response, - ignore_users, - labels_to_measure, + ignore_users_list, + labels_to_measure_list, search_query, ) diff --git a/issue_metrics.py b/issue_metrics.py index df911c2a..8f0c0483 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -125,7 +125,7 @@ def get_per_issue_metrics( issues: Union[List[dict], List[github3.search.IssueSearchResult]], # type: ignore discussions: bool = False, labels: Union[List[str], None] = None, - ignore_users: List[str] = None, + ignore_users: List[str] = [], ) -> tuple[List, int, int]: """ Calculate the metrics for each issue/pr/discussion in a list provided. diff --git a/labels.py b/labels.py index fe1c8551..9a5a9e7c 100644 --- a/labels.py +++ b/labels.py @@ -41,7 +41,7 @@ def get_label_metrics(issue: github3.issues.Issue, labels: List[str]) -> dict: Returns: dict: A dictionary containing the time spent in each label or None. """ - label_metrics = {} + label_metrics: dict = {} label_events = get_label_events(issue, labels) for label in labels: diff --git a/requirements-test.txt b/requirements-test.txt index 4b3c2dbd..c0183053 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,5 +1,9 @@ black==24.3.0 flake8==7.0.0 +mypy==1.8.0 +mypy-extensions==1.0.0 pylint==3.1.0 pytest==8.1.1 pytest-cov==4.1.0 +types-pytz==2024.1.0.20240203 +types-requests==2.31.0.20240311 diff --git a/time_to_first_response.py b/time_to_first_response.py index e3427edb..b067c310 100644 --- a/time_to_first_response.py +++ b/time_to_first_response.py @@ -31,7 +31,7 @@ def measure_time_to_first_response( discussion: Union[dict, None], pull_request: Union[github3.pulls.PullRequest, None] = None, ready_for_review_at: Union[datetime, None] = None, - ignore_users: List[str] = None, + ignore_users: List[str] = [], ) -> Union[timedelta, None]: """Measure the time to first response for a single issue, pull request, or a discussion. @@ -136,7 +136,7 @@ def ignore_comment( def get_stats_time_to_first_response( issues: List[IssueWithMetrics], -) -> Union[timedelta, None]: +) -> Union[dict[str, timedelta], None]: """Calculate the stats describing time to first response for a list of issues. Args: From f0613ee64fd346221862a0f1494ecd20176698b0 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Sat, 23 Mar 2024 23:54:48 -0500 Subject: [PATCH 006/358] chore: fix incompatable default argument Signed-off-by: jmeridth --- issue_metrics.py | 2 +- time_to_first_response.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index 8f0c0483..14836127 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -125,7 +125,7 @@ def get_per_issue_metrics( issues: Union[List[dict], List[github3.search.IssueSearchResult]], # type: ignore discussions: bool = False, labels: Union[List[str], None] = None, - ignore_users: List[str] = [], + ignore_users: Union[List[str], None] = None, ) -> tuple[List, int, int]: """ Calculate the metrics for each issue/pr/discussion in a list provided. diff --git a/time_to_first_response.py b/time_to_first_response.py index b067c310..65417bae 100644 --- a/time_to_first_response.py +++ b/time_to_first_response.py @@ -31,7 +31,7 @@ def measure_time_to_first_response( discussion: Union[dict, None], pull_request: Union[github3.pulls.PullRequest, None] = None, ready_for_review_at: Union[datetime, None] = None, - ignore_users: List[str] = [], + ignore_users: Union[List[str], None] = None, ) -> Union[timedelta, None]: """Measure the time to first response for a single issue, pull request, or a discussion. From be1272d733112a8ac3fe7920cfa388b47f927b3f Mon Sep 17 00:00:00 2001 From: jmeridth Date: Sun, 24 Mar 2024 00:25:37 -0500 Subject: [PATCH 007/358] fix: linting, consistent return types, we got this Signed-off-by: jmeridth --- json_writer.py | 13 +++++++------ time_to_answer.py | 2 +- time_to_close.py | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/json_writer.py b/json_writer.py index 8f80e6f0..5ee160a3 100644 --- a/json_writer.py +++ b/json_writer.py @@ -105,12 +105,13 @@ def write_to_json( average_time_in_labels = {} med_time_in_labels = {} p90_time_in_labels = {} - for label, time in stats_time_in_labels["avg"].items(): - average_time_in_labels[label] = str(time) - for label, time in stats_time_in_labels["med"].items(): - med_time_in_labels[label] = str(time) - for label, time in stats_time_in_labels["90p"].items(): - p90_time_in_labels[label] = str(time) + if stats_time_in_labels is not None: + for label, time in stats_time_in_labels["avg"].items(): + average_time_in_labels[label] = str(time) + for label, time in stats_time_in_labels["med"].items(): + med_time_in_labels[label] = str(time) + for label, time in stats_time_in_labels["90p"].items(): + p90_time_in_labels[label] = str(time) # Create a dictionary with the metrics metrics = { diff --git a/time_to_answer.py b/time_to_answer.py index 9e330a51..248be58d 100644 --- a/time_to_answer.py +++ b/time_to_answer.py @@ -24,7 +24,7 @@ def get_stats_time_to_answer( issues_with_metrics: List[IssueWithMetrics], -) -> Union[timedelta, None]: +) -> Union[dict[str, timedelta], None]: """ Calculate stats describing the time to answer for a list of issues. """ diff --git a/time_to_close.py b/time_to_close.py index 6887aa15..a32262d1 100644 --- a/time_to_close.py +++ b/time_to_close.py @@ -57,7 +57,7 @@ def measure_time_to_close( def get_stats_time_to_close( issues_with_metrics: List[IssueWithMetrics], -) -> Union[timedelta, None]: +) -> Union[dict[str, timedelta], None]: """Calculate stats describing the time to close for a list of issues. Args: From eed2aaa233d57e48410d5588caa32b5b1480187b Mon Sep 17 00:00:00 2001 From: jmeridth Date: Sun, 24 Mar 2024 12:45:51 -0500 Subject: [PATCH 008/358] fix: more linting, 3 to go replace comments with variables, causes ensured maintenance of meaning Signed-off-by: jmeridth --- json_writer.py | 4 ++-- labels.py | 10 +++++----- test_time_to_answer.py | 2 +- time_to_first_response.py | 30 ++++++++++++++++++------------ 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/json_writer.py b/json_writer.py index 5ee160a3..1955ef39 100644 --- a/json_writer.py +++ b/json_writer.py @@ -17,7 +17,7 @@ import json import os from datetime import timedelta -from typing import List, Union +from typing import Any, List, Union from classes import IssueWithMetrics @@ -114,7 +114,7 @@ def write_to_json( p90_time_in_labels[label] = str(time) # Create a dictionary with the metrics - metrics = { + metrics: dict[str, Any] = { "average_time_to_first_response": str(average_time_to_first_response), "average_time_to_close": str(average_time_to_close), "average_time_to_answer": str(average_time_to_answer), diff --git a/labels.py b/labels.py index 9a5a9e7c..6fd6da60 100644 --- a/labels.py +++ b/labels.py @@ -91,8 +91,8 @@ def get_label_metrics(issue: github3.issues.Issue, labels: List[str]) -> dict: def get_stats_time_in_labels( issues_with_metrics: List[IssueWithMetrics], - labels: List[str], -) -> dict[str, timedelta]: + labels: dict[str, timedelta], +) -> dict[str, dict[str, timedelta | None ]]: """Calculate stats describing time spent in each label.""" time_in_labels = {} for issue in issues_with_metrics: @@ -107,9 +107,9 @@ def get_stats_time_in_labels( issue.label_metrics[label].total_seconds() ) - average_time_in_labels = {} - med_time_in_labels = {} - ninety_percentile_in_labels = {} + average_time_in_labels: dict[str, timedelta | None] = {} + med_time_in_labels: dict[str, timedelta | None] = {} + ninety_percentile_in_labels: dict[str, timedelta | None] = {} for label, time_list in time_in_labels.items(): average_time_in_labels[label] = timedelta( seconds=numpy.round(numpy.average(time_list)) diff --git a/test_time_to_answer.py b/test_time_to_answer.py index 5d5a44f9..c6b33e32 100644 --- a/test_time_to_answer.py +++ b/test_time_to_answer.py @@ -20,7 +20,7 @@ class TestGetAverageTimeToAnswer(unittest.TestCase): def test_returns_none_for_empty_list(self): """Tests that the function returns None when given an empty list of issues.""" # Arrange - issues_with_metrics: List[IssueWithMetrics] = [] + issues_with_metrics = [] # Act result = get_stats_time_to_answer(issues_with_metrics) diff --git a/time_to_first_response.py b/time_to_first_response.py index 65417bae..4facf5e3 100644 --- a/time_to_first_response.py +++ b/time_to_first_response.py @@ -99,7 +99,7 @@ def measure_time_to_first_response( if ready_for_review_at: issue_time = ready_for_review_at else: - issue_time = datetime.fromisoformat(issue.created_at) # type: ignore + issue_time = datetime.fromisoformat(issue.created_at) if discussion and len(discussion["comments"]["nodes"]) > 0: earliest_response = datetime.fromisoformat( @@ -107,9 +107,11 @@ def measure_time_to_first_response( ) issue_time = datetime.fromisoformat(discussion["createdAt"]) - # Calculate the time between the issue and the first comment if earliest_response and issue_time: - return earliest_response - issue_time + time_between_issue_and_first_comment: timedelta | None = ( + earliest_response - issue_time + ) + return time_between_issue_and_first_comment return None @@ -122,16 +124,20 @@ def ignore_comment( ready_for_review_at: Union[datetime, None], ) -> bool: """Check if a comment should be ignored.""" - return ( - # ignore comments by IGNORE_USERS - comment_user.login in ignore_users - # ignore comments by bots - or comment_user.type == "Bot" - # ignore comments by the issue creator - or comment_user.login == issue_user.login - # ignore comments created before the issue was ready for review - or (ready_for_review_at and comment_created_at < ready_for_review_at) + + user_is_ignored: bool = comment_user.login in ignore_users + user_is_a_bot: bool = str(comment_user.type.lower()) == "bot" + user_is_issue_creator: bool = str(comment_user.login) == str(issue_user.login) + issue_was_created_before_ready_for_review: bool = ( + ready_for_review_at and (comment_created_at < ready_for_review_at) + ) + result: bool = ( + user_is_ignored + or user_is_a_bot + or user_is_issue_creator + or issue_was_created_before_ready_for_review ) + return result def get_stats_time_to_first_response( From ca8e41680dffb299f180f7f44571fe110a0eb8ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 17:40:17 +0000 Subject: [PATCH 009/358] build(deps): bump pytest-cov from 4.1.0 to 5.0.0 Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 4.1.0 to 5.0.0. - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v4.1.0...v5.0.0) --- updated-dependencies: - dependency-name: pytest-cov dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7fe0965b..a423ee11 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ python-dotenv==1.0.1 pytz==2024.1 Requests==2.31.0 pytest==8.1.1 -pytest-cov==4.1.0 +pytest-cov==5.0.0 flake8==7.0.0 pylint==3.1.0 numpy==1.26.4 From ccfae8b30ed97a8fb1dab571e9a835ee0ca6ac37 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Tue, 26 Mar 2024 05:57:55 -0500 Subject: [PATCH 010/358] fix: linting, still 3 mypy errors, very close Signed-off-by: jmeridth --- README.md | 4 +++- labels.py | 2 +- requirements.txt | 8 ++------ test_time_to_answer.py | 1 - time_to_first_response.py | 4 ++-- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 7f7ceb08..b89c61d1 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,9 @@ If you need support using this project or have questions about it, please [open 2. Select a best fit workflow file from the [examples directory](./docs/example-workflows.md) for your use case. 3. Copy that example into your repository (from step 1) and into the proper directory for GitHub Actions: `.github/workflows/` directory with the file extension `.yml` (ie. `.github/workflows/issue-metrics.yml`) 4. Edit the values (`SEARCH_QUERY`, `assignees`) from the sample workflow with your information. See the [SEARCH_QUERY](./docs/search-query.md) section for more information on how to configure the search query. -5. If you are running metrics on a repository other than the one where the workflow file is going to be, then update the value of `GH_TOKEN`. Do this by creating a [GitHub API token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic) with permissions to read the repository and write issues. Then take the value of the API token you just created, and [create a repository secret](https://docs.github.com/en/actions/security-guides/encrypted-secrets) where the name of the secret is `GH_TOKEN` and the value of the secret the API token. Then finally update the workflow file to use that repository secret by changing `GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}` to `GH_TOKEN: ${{ secrets.GH_TOKEN }}`. The name of the secret can really be anything. It just needs to match between when you create the secret name and when you refer to it in the workflow file. +5. If you are running metrics on a repository other than the one where the workflow file is going to be, then update the value of `GH_TOKEN`. + - Do this by creating a [GitHub API token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic) with permissions to read the repository and write issues. + - Then take the value of the API token you just created, and [create a repository secret](https://docs.github.com/en/actions/security-guides/encrypted-secrets) where the name of the secret is `GH_TOKEN` and the value of the secret the API token. Then finally update the workflow file to use that repository secret by changing `GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}` to `GH_TOKEN: ${{ secrets.GH_TOKEN }}`. The name of the secret can really be anything. It just needs to match between when you create the secret name and when you refer to it in the workflow file. 6. If you want the resulting issue with the metrics in it to appear in a different repository other than the one the workflow file runs in, update the line `token: ${{ secrets.GITHUB_TOKEN }}` with your own GitHub API token stored as a repository secret. This process is the same as described in the step above. More info on creating secrets can be found [here](https://docs.github.com/en/actions/security-guides/encrypted-secrets). 7. Commit the workflow file to the default branch (often `master` or `main`) 8. Wait for the action to trigger based on the `schedule` entry or manually trigger the workflow as shown in the [documentation](https://docs.github.com/en/actions/using-workflows/manually-running-a-workflow). diff --git a/labels.py b/labels.py index 6fd6da60..2bfcab38 100644 --- a/labels.py +++ b/labels.py @@ -92,7 +92,7 @@ def get_label_metrics(issue: github3.issues.Issue, labels: List[str]) -> dict: def get_stats_time_in_labels( issues_with_metrics: List[IssueWithMetrics], labels: dict[str, timedelta], -) -> dict[str, dict[str, timedelta | None ]]: +) -> dict[str, dict[str, timedelta | None]]: """Calculate stats describing time spent in each label.""" time_in_labels = {} for issue in issues_with_metrics: diff --git a/requirements.txt b/requirements.txt index a423ee11..c1d2d5fc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,5 @@ github3.py==4.0.1 +numpy==1.26.4 python-dotenv==1.0.1 pytz==2024.1 -Requests==2.31.0 -pytest==8.1.1 -pytest-cov==5.0.0 -flake8==7.0.0 -pylint==3.1.0 -numpy==1.26.4 +requests==2.31.0 diff --git a/test_time_to_answer.py b/test_time_to_answer.py index c6b33e32..225fc012 100644 --- a/test_time_to_answer.py +++ b/test_time_to_answer.py @@ -2,7 +2,6 @@ import unittest from datetime import timedelta -from typing import List from classes import IssueWithMetrics from time_to_answer import get_stats_time_to_answer, measure_time_to_answer diff --git a/time_to_first_response.py b/time_to_first_response.py index 4facf5e3..592899cb 100644 --- a/time_to_first_response.py +++ b/time_to_first_response.py @@ -128,8 +128,8 @@ def ignore_comment( user_is_ignored: bool = comment_user.login in ignore_users user_is_a_bot: bool = str(comment_user.type.lower()) == "bot" user_is_issue_creator: bool = str(comment_user.login) == str(issue_user.login) - issue_was_created_before_ready_for_review: bool = ( - ready_for_review_at and (comment_created_at < ready_for_review_at) + issue_was_created_before_ready_for_review: bool = ready_for_review_at and ( + comment_created_at < ready_for_review_at ) result: bool = ( user_is_ignored From 6276ac0dbce7d51222cacf4f103895afd41aed7a Mon Sep 17 00:00:00 2001 From: jmeridth Date: Tue, 26 Mar 2024 06:36:31 -0500 Subject: [PATCH 011/358] fix: linting, 2 down 1 to go Signed-off-by: jmeridth --- issue_metrics.py | 2 +- time_to_first_response.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index 14836127..0a2cf173 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -181,7 +181,7 @@ def get_per_issue_metrics( # Check if issue is actually a pull request pull_request, ready_for_review_at = None, None - if issue.issue.pull_request_urls: + if issue.issue.pull_request_urls: # type: ignore pull_request = issue.issue.pull_request() ready_for_review_at = get_time_to_ready_for_review(issue, pull_request) diff --git a/time_to_first_response.py b/time_to_first_response.py index 592899cb..46dc3b89 100644 --- a/time_to_first_response.py +++ b/time_to_first_response.py @@ -128,9 +128,11 @@ def ignore_comment( user_is_ignored: bool = comment_user.login in ignore_users user_is_a_bot: bool = str(comment_user.type.lower()) == "bot" user_is_issue_creator: bool = str(comment_user.login) == str(issue_user.login) - issue_was_created_before_ready_for_review: bool = ready_for_review_at and ( - comment_created_at < ready_for_review_at - ) + issue_was_created_before_ready_for_review: bool = False + if ready_for_review_at: + issue_was_created_before_ready_for_review = ( + comment_created_at < ready_for_review_at + ) result: bool = ( user_is_ignored or user_is_a_bot From 8c81f5136ea37f148d016072bfd44cdbbc7c15d0 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Tue, 26 Mar 2024 06:54:33 -0500 Subject: [PATCH 012/358] fix: markdown linting Signed-off-by: jmeridth --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b89c61d1..ec7442ae 100644 --- a/README.md +++ b/README.md @@ -101,8 +101,10 @@ If you need support using this project or have questions about it, please [open 4. Edit the values (`SEARCH_QUERY`, `assignees`) from the sample workflow with your information. See the [SEARCH_QUERY](./docs/search-query.md) section for more information on how to configure the search query. 5. If you are running metrics on a repository other than the one where the workflow file is going to be, then update the value of `GH_TOKEN`. - Do this by creating a [GitHub API token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic) with permissions to read the repository and write issues. - - Then take the value of the API token you just created, and [create a repository secret](https://docs.github.com/en/actions/security-guides/encrypted-secrets) where the name of the secret is `GH_TOKEN` and the value of the secret the API token. Then finally update the workflow file to use that repository secret by changing `GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}` to `GH_TOKEN: ${{ secrets.GH_TOKEN }}`. The name of the secret can really be anything. It just needs to match between when you create the secret name and when you refer to it in the workflow file. -6. If you want the resulting issue with the metrics in it to appear in a different repository other than the one the workflow file runs in, update the line `token: ${{ secrets.GITHUB_TOKEN }}` with your own GitHub API token stored as a repository secret. This process is the same as described in the step above. More info on creating secrets can be found [here](https://docs.github.com/en/actions/security-guides/encrypted-secrets). + - Then take the value of the API token you just created, and [create a repository secret](https://docs.github.com/en/actions/security-guides/encrypted-secrets) where the name of the secret is `GH_TOKEN` and the value of the secret the API token. + - Then finally update the workflow file to use that repository secret by changing `GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}` to `GH_TOKEN: ${{ secrets.GH_TOKEN }}`. The name of the secret can really be anything. It just needs to match between when you create the secret name and when you refer to it in the workflow file. +6. If you want the resulting issue with the metrics in it to appear in a different repository other than the one the workflow file runs in, update the line `token: ${{ secrets.GITHUB_TOKEN }}` with your own GitHub API token stored as a repository secret. + - This process is the same as described in the step above. More info on creating secrets can be found [here](https://docs.github.com/en/actions/security-guides/encrypted-secrets). 7. Commit the workflow file to the default branch (often `master` or `main`) 8. Wait for the action to trigger based on the `schedule` entry or manually trigger the workflow as shown in the [documentation](https://docs.github.com/en/actions/using-workflows/manually-running-a-workflow). From ad44a93d9080fd28b33c022654d6900bc3339fc1 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Tue, 26 Mar 2024 07:38:50 -0500 Subject: [PATCH 013/358] fix: linting by ignoring - will reassess later Signed-off-by: jmeridth --- issue_metrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/issue_metrics.py b/issue_metrics.py index 0a2cf173..bd4d040e 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -182,7 +182,7 @@ def get_per_issue_metrics( # Check if issue is actually a pull request pull_request, ready_for_review_at = None, None if issue.issue.pull_request_urls: # type: ignore - pull_request = issue.issue.pull_request() + pull_request = issue.issue.pull_request() # type: ignore ready_for_review_at = get_time_to_ready_for_review(issue, pull_request) issue_with_metrics.time_to_first_response = measure_time_to_first_response( From 2e3a8c862c29411519ecc1d2980dea2fd2cbf9ee Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 26 Mar 2024 16:10:34 -0700 Subject: [PATCH 014/358] switch to v3 --- docs/search-query.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/search-query.md b/docs/search-query.md index 61c5c893..64a43f64 100644 --- a/docs/search-query.md +++ b/docs/search-query.md @@ -64,7 +64,7 @@ jobs: steps: - name: Run issue-metrics tool for issues and prs opened in May 2023 - uses: github/issue-metrics@v2 + uses: github/issue-metrics@v3 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SEARCH_QUERY: 'repo:owner/repo created:2023-05-01..2023-05-31 -reason:"not planned"' @@ -78,7 +78,7 @@ jobs: assignees: - name: Run issue-metrics tool for issues and prs closed in May 2023 - uses: github/issue-metrics@v2 + uses: github/issue-metrics@v3 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SEARCH_QUERY: 'repo:owner/repo closed:2023-05-01..2023-05-31 -reason:"not planned"' From 70fe26000eae8853d503003e32c0a5f636d39fbd Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 26 Mar 2024 16:11:33 -0700 Subject: [PATCH 015/358] v3 bump --- action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yml b/action.yml index 3e5aa541..73a9942d 100644 --- a/action.yml +++ b/action.yml @@ -4,7 +4,7 @@ author: 'github' description: 'A GitHub Action to report out issue metrics' runs: using: 'docker' - image: 'docker://ghcr.io/github/issue_metrics:v2' + image: 'docker://ghcr.io/github/issue_metrics:v3' branding: icon: 'check-square' color: 'white' From db515204fefa76b73d04dc52e07ea6bb2f587ac6 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 26 Mar 2024 16:12:01 -0700 Subject: [PATCH 016/358] v3 bump --- docs/assign-team-instead-of-individual.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/assign-team-instead-of-individual.md b/docs/assign-team-instead-of-individual.md index 9581a132..57f0f0d2 100644 --- a/docs/assign-team-instead-of-individual.md +++ b/docs/assign-team-instead-of-individual.md @@ -46,7 +46,7 @@ jobs: echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV" - name: Run issue-metrics tool - uses: github/issue-metrics@v2 + uses: github/issue-metrics@v3 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SEARCH_QUERY: 'repo:owner/repo is:issue created:${{ env.last_month }} -reason:"not planned"' From 8c40575e9caae3eae71f69416303678a32b9a4db Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 26 Mar 2024 16:12:53 -0700 Subject: [PATCH 017/358] v3 bump --- docs/authenticating-with-github-app-installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/authenticating-with-github-app-installation.md b/docs/authenticating-with-github-app-installation.md index ed62042b..3ba2e3f5 100644 --- a/docs/authenticating-with-github-app-installation.md +++ b/docs/authenticating-with-github-app-installation.md @@ -38,7 +38,7 @@ jobs: echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV" - name: Run issue-metrics tool - uses: github/issue-metrics@v2 + uses: github/issue-metrics@v3 env: GH_APP_ID: ${{ secrets.GITHUB_APP_ID }} GH_APP_INSTALLATION_ID: ${{ secrets.GITHUB_APP_INSTALLATION_ID }} From ed3b12eb33141d3670aab43565c03bd1668dd81a Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 26 Mar 2024 16:13:33 -0700 Subject: [PATCH 018/358] v3 bump --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ec7442ae..f18668cb 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ jobs: echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV" - name: Run issue-metrics tool - uses: github/issue-metrics@v2 + uses: github/issue-metrics@v3 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SEARCH_QUERY: 'repo:owner/repo is:issue created:${{ env.last_month }} -reason:"not planned"' From 811a810c57824ccfb5afb5942762578130a80c70 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 26 Mar 2024 16:14:23 -0700 Subject: [PATCH 019/358] v3 bump --- docs/measure-time.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/measure-time.md b/docs/measure-time.md index cfa86e2c..fb64bcb6 100644 --- a/docs/measure-time.md +++ b/docs/measure-time.md @@ -25,7 +25,7 @@ jobs: steps: - name: Run issue-metrics tool - uses: github/issue-metrics@v2 + uses: github/issue-metrics@v3 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} LABELS_TO_MEASURE: 'waiting-for-manager-approval,waiting-for-security-review' From f090ae3a28fdb77cfb6142d2799ef9fbf8e78d12 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 26 Mar 2024 16:14:46 -0700 Subject: [PATCH 020/358] v3 bump --- docs/example-using-json-instead-markdown-output.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/example-using-json-instead-markdown-output.md b/docs/example-using-json-instead-markdown-output.md index 7d82fcd9..3995b0fb 100644 --- a/docs/example-using-json-instead-markdown-output.md +++ b/docs/example-using-json-instead-markdown-output.md @@ -23,7 +23,7 @@ jobs: steps: - name: Run issue-metrics tool id: issue-metrics - uses: github/issue-metrics@v2 + uses: github/issue-metrics@v3 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SEARCH_QUERY: 'repo:owner/repo is:issue created:2023-05-01..2023-05-31 -reason:"not planned"' From a0261dfa7d020810468e4ad17bb7754d01972426 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 26 Mar 2024 16:15:56 -0700 Subject: [PATCH 021/358] v3 bump --- docs/example-workflows.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/example-workflows.md b/docs/example-workflows.md index 69656324..c0ee9d7c 100644 --- a/docs/example-workflows.md +++ b/docs/example-workflows.md @@ -38,7 +38,7 @@ jobs: echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV" - name: Run issue-metrics tool - uses: github/issue-metrics@v2 + uses: github/issue-metrics@v3 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SEARCH_QUERY: 'repo:owner/repo is:issue created:${{ env.last_month }} -reason:"not planned"' @@ -76,7 +76,7 @@ jobs: steps: - name: Run issue-metrics tool - uses: github/issue-metrics@v2 + uses: github/issue-metrics@v3 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SEARCH_QUERY: 'repo:owner/repo is:issue created:2023-05-01..2023-05-31 -reason:"not planned"' @@ -126,7 +126,7 @@ jobs: echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV" - name: Get issue metrics - uses: github/issue-metrics@v2 + uses: github/issue-metrics@v3 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SEARCH_QUERY: 'repo:owner/repo1 repo:owner/repo2 is:issue created:${{ env.last_month }} -reason:"not planned"' From ed01a22309ca09fb7b9360447aeede4f7905522d Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 26 Mar 2024 16:18:43 -0700 Subject: [PATCH 022/358] hotfix for codeql build --- .github/workflows/codeql-analysis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 26d0c92c..78ba9133 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -23,6 +23,7 @@ on: permissions: contents: read + security-events: write jobs: analyze: From 1220b749fe73a9d2e8fb9ea913a47e4a64b0ef3f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Mar 2024 17:20:37 +0000 Subject: [PATCH 023/358] build(deps): bump pytest-cov from 4.1.0 to 5.0.0 Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 4.1.0 to 5.0.0. - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v4.1.0...v5.0.0) --- updated-dependencies: - dependency-name: pytest-cov dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index c0183053..2db216bb 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -4,6 +4,6 @@ mypy==1.8.0 mypy-extensions==1.0.0 pylint==3.1.0 pytest==8.1.1 -pytest-cov==4.1.0 +pytest-cov==5.0.0 types-pytz==2024.1.0.20240203 types-requests==2.31.0.20240311 From af0a53a715d8eddc56bd44d288976ccb00808f07 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Mar 2024 17:49:16 +0000 Subject: [PATCH 024/358] build(deps): bump mypy from 1.8.0 to 1.9.0 Bumps [mypy](https://github.com/python/mypy) from 1.8.0 to 1.9.0. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.8.0...1.9.0) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 2db216bb..e17819bb 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,6 +1,6 @@ black==24.3.0 flake8==7.0.0 -mypy==1.8.0 +mypy==1.9.0 mypy-extensions==1.0.0 pylint==3.1.0 pytest==8.1.1 From d5e644965da6120e43df3cfe21563e007ff3eb9a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:43:13 +0000 Subject: [PATCH 025/358] build(deps): bump types-requests from 2.31.0.20240311 to 2.31.0.20240403 Bumps [types-requests](https://github.com/python/typeshed) from 2.31.0.20240311 to 2.31.0.20240403. - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index e17819bb..7077314f 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -6,4 +6,4 @@ pylint==3.1.0 pytest==8.1.1 pytest-cov==5.0.0 types-pytz==2024.1.0.20240203 -types-requests==2.31.0.20240311 +types-requests==2.31.0.20240403 From eca676bbf1124a580adeb1a29d10457b498baace Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 17:23:15 +0000 Subject: [PATCH 026/358] build(deps): bump types-requests from 2.31.0.20240403 to 2.31.0.20240406 Bumps [types-requests](https://github.com/python/typeshed) from 2.31.0.20240403 to 2.31.0.20240406. - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 7077314f..f9457d71 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -6,4 +6,4 @@ pylint==3.1.0 pytest==8.1.1 pytest-cov==5.0.0 types-pytz==2024.1.0.20240203 -types-requests==2.31.0.20240403 +types-requests==2.31.0.20240406 From 204828ec4c11bf0ea68159de994b1e109031f813 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Wed, 10 Apr 2024 14:16:28 -0500 Subject: [PATCH 027/358] docs: add github issue templates Closes #231 - [x] Issue Templates - [x] bug report - [x] feature request - [x] config linking to discussions and github-ospo issues for overall issue filing - [x] Update README linking to github-ospo repo issues for OSPO GitHub Actions issues as a whole (https://github.com/github/github-ospo/issues/75) Signed-off-by: jmeridth --- .github/ISSUE_TEMPLATE/bug_report.yml | 45 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 9 +++++ .github/ISSUE_TEMPLATE/feature_request.yml | 35 +++++++++++++++++ README.md | 4 ++ 4 files changed, 93 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..a0e96428 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,45 @@ +--- +name: Bug report +description: Create a report to help us improve +labels: + - bug +body: + - type: textarea + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is. + validations: + required: true + + - type: textarea + attributes: + label: To Reproduce + description: Steps to reproduce the behavior + placeholder: | + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + validations: + required: true + + - type: textarea + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen. + validations: + required: true + + - type: textarea + attributes: + label: Screenshots + description: If applicable, add screenshots to help explain your problem. + validations: + required: false + + - type: textarea + attributes: + label: Additional context + description: Add any other context about the problem here. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..f9eb1679 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,9 @@ +blank_issues_enabled: false + +contact_links: + - name: Ask a question + url: https://github.com/github/issue-metrics/discussions/new + about: Ask a question or start a discussion + - name: GitHub OSPO GitHub Action Overall Issue + url: https://github.com/github/github-ospo/issues/new + about: File issue for multiple GitHub OSPO GitHub Actions diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..6bfc5a7d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,35 @@ +--- +name: Feature request +description: Suggest an idea for this project +labels: + - enhancement +body: + - type: textarea + attributes: + label: Is your feature request related to a problem? + description: A clear and concise description of what the problem is. Please describe. + placeholder: | + Ex. I'm always frustrated when [...] + validations: + required: false + + - type: textarea + attributes: + label: Describe the solution you'd like + description: A clear and concise description of what you want to happen. + validations: + required: true + + - type: textarea + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered. + validations: + required: false + + - type: textarea + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. + validations: + required: false diff --git a/README.md b/README.md index f18668cb..028d0e7e 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,10 @@ jobs: If you need support using this project or have questions about it, please [open up an issue in this repository](https://github.com/github/issue-metrics/issues). Requests made directly to GitHub staff or support team will be redirected here to open an issue. GitHub SLA's and support/services contracts do not apply to this repository. +### OSPO GitHub Actions as a Whole + +All feedback regarding our GitHub Actions, as a whole, should be communicated through [issues on our github-ospo repository](https://github.com/github/github-ospo/issues/new). + ## Use as a GitHub Action 1. Create a repository to host this GitHub Action or select an existing repository. This is easiest if it is the same repository as the one you want to measure metrics on. From eb98ac51058b593311088c451c727f4bbe77eed1 Mon Sep 17 00:00:00 2001 From: Isabel Drost-Fromm Date: Thu, 11 Apr 2024 07:02:44 +0200 Subject: [PATCH 028/358] First stab at figuring out the number of very active mentors (#206) * First stab at figuring out the number of very active mentors in a project. * Fix format and linter errors. * Add flake to requirements. * Adds call to mentor counter and output in markdown. This adds the call to mentor counter and displays the results in markdown including first tests for this functionality. * Make mentor counting configurable. This adds two configuration options: One to enable mentor counting and one for configuring how many comments a user needs to leave in discussions, PRs. and issues to be counted as an active mentor. * Adds mentor counting to json output and adds missing config. This adds mentor counting output to json format. In addition this change makes max number of comments to evaluate configurable as well as the cutoff for heavily involved mentors. * Fix merge conflicts. * Fix linting errors. * fix: linting fixes Signed-off-by: Zack Koppert * 8 is reasonable number of attrs Signed-off-by: Zack Koppert * Update test_most_active_mentors.py Co-authored-by: Jason Meridth * Update config.py Co-authored-by: Jason Meridth * Update config.py Remove merge residual * Update requirements.txt Remove lib only needed for testing. * Update issue_metrics.py Co-authored-by: Jason Meridth * Update config.py * Update config.py set type of `enable_mentor_count` to `bool` * Update test_config.py change tests to handle boolean change of enable_mentor_count --------- Signed-off-by: Zack Koppert Co-authored-by: Drost-Fromm Co-authored-by: Zack Koppert Co-authored-by: Jason Meridth Co-authored-by: Jason Meridth --- README.md | 4 + classes.py | 5 + config.py | 24 +++++ issue_metrics.py | 40 +++++++- json_writer.py | 3 + markdown_writer.py | 8 +- most_active_mentors.py | 179 ++++++++++++++++++++++++++++++++++++ test_config.py | 12 +++ test_issue_metrics.py | 4 +- test_json_writer.py | 6 ++ test_markdown_writer.py | 11 ++- test_most_active_mentors.py | 82 +++++++++++++++++ 12 files changed, 370 insertions(+), 8 deletions(-) create mode 100755 most_active_mentors.py create mode 100755 test_most_active_mentors.py diff --git a/README.md b/README.md index 028d0e7e..801ea852 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,10 @@ This action can be configured to authenticate with GitHub App Installation or Pe | `HIDE_TIME_TO_CLOSE` | False | False | If set to `true`, the time to close will not be displayed in the generated Markdown file. | | `HIDE_TIME_TO_FIRST_RESPONSE` | False | False | If set to `true`, the time to first response will not be displayed in the generated Markdown file. | | `IGNORE_USERS` | False | False | A comma separated list of users to ignore when calculating metrics. (ie. `IGNORE_USERS: 'user1,user2'`). To ignore bots, append `[bot]` to the user (ie. `IGNORE_USERS: 'github-actions[bot]'`) | +| `ENABLE_MENTOR_COUNT` | False | False | If set to 'TRUE' count number of comments users left on discussions, issues and PRs and display number of active mentors | +| `MIN_MENTOR_COMMENTS` | False | 10 | Minimum number of comments to count as a mentor | +| `MAX_COMMENTS_EVAL` | False | 20 | Maximum number of comments per thread to evaluate for mentor stats | +| `HEAVILY_INVOLVED_CUTOFF` | False | 3 | Cutoff after which a mentor's comments in one issue are no longer counted against their total score | | `LABELS_TO_MEASURE` | False | `""` | A comma separated list of labels to measure how much time the label is applied. If not provided, no labels durations will be measured. Not compatible with discussions at this time. | | `SEARCH_QUERY` | True | `""` | The query by which you can filter issues/PRs which must contain a `repo:`, `org:`, `owner:`, or a `user:` entry. For discussions, include `type:discussions` in the query. | diff --git a/classes.py b/classes.py index 3867f261..e1e6684d 100644 --- a/classes.py +++ b/classes.py @@ -19,9 +19,12 @@ class IssueWithMetrics: time_to_answer (timedelta, optional): The time it took to answer the discussions in the issue. label_metrics (dict, optional): A dictionary containing the label metrics + mentor_activity (dict, optional): A dictionary containing active mentors """ + # pylint: disable=too-many-instance-attributes + def __init__( self, title, @@ -31,6 +34,7 @@ def __init__( time_to_close=None, time_to_answer=None, labels_metrics=None, + mentor_activity=None, ): self.title = title self.html_url = html_url @@ -39,3 +43,4 @@ def __init__( self.time_to_close = time_to_close self.time_to_answer = time_to_answer self.label_metrics = labels_metrics + self.mentor_activity = mentor_activity diff --git a/config.py b/config.py index e7d87be2..30e91aa7 100644 --- a/config.py +++ b/config.py @@ -34,6 +34,10 @@ class EnvVars: hide_time_to_first_response (bool): If true, the time to first response metric is hidden in the output ignore_users (List[str]): List of usernames to ignore when calculating metrics labels_to_measure (List[str]): List of labels to measure how much time the lable is applied + enable_mentor_count (bool): If set to TRUE, compute number of mentors + min_mentor_comments (str): If set, defines the minimum number of comments for mentors + max_comments_eval (str): If set, defines the maximum number of comments to look at for mentor evaluation + heavily_involved_cutoff (str): If set, defines the cutoff after which heavily involved commentors in search_query (str): Search query used to filter issues/prs/discussions on GitHub """ @@ -51,6 +55,10 @@ def __init__( hide_time_to_first_response: bool, ignore_user: List[str], labels_to_measure: List[str], + enable_mentor_count: bool, + min_mentor_comments: str, + max_comments_eval: str, + heavily_involved_cutoff: str, search_query: str, ): self.gh_app_id = gh_app_id @@ -65,6 +73,10 @@ def __init__( self.hide_time_to_answer = hide_time_to_answer self.hide_time_to_close = hide_time_to_close self.hide_time_to_first_response = hide_time_to_first_response + self.enable_mentor_count = enable_mentor_count + self.min_mentor_comments = min_mentor_comments + self.max_comments_eval = max_comments_eval + self.heavily_involved_cutoff = heavily_involved_cutoff self.search_query = search_query def __repr__(self): @@ -82,6 +94,10 @@ def __repr__(self): f"{self.hide_time_to_first_response}," f"{self.ignore_users}," f"{self.labels_to_measure}," + f"{self.enable_mentor_count}," + f"{self.min_mentor_comments}," + f"{self.max_comments_eval}," + f"{self.heavily_involved_cutoff}," f"{self.search_query})" ) @@ -166,6 +182,10 @@ def get_env_vars(test: bool = False) -> EnvVars: hide_time_to_answer = get_bool_env_var("HIDE_TIME_TO_ANSWER") hide_time_to_close = get_bool_env_var("HIDE_TIME_TO_CLOSE") hide_time_to_first_response = get_bool_env_var("HIDE_TIME_TO_FIRST_RESPONSE") + enable_mentor_count = get_bool_env_var("ENABLE_MENTOR_COUNT") + min_mentor_comments = os.getenv("MIN_MENTOR_COMMENTS", "10") + max_comments_eval = os.getenv("MAX_COMMENTS_EVAL", "20") + heavily_involved_cutoff = os.getenv("HEAVILY_INVOLVED_CUTOFF", "3") return EnvVars( gh_app_id, @@ -180,5 +200,9 @@ def get_env_vars(test: bool = False) -> EnvVars: hide_time_to_first_response, ignore_users_list, labels_to_measure_list, + enable_mentor_count, + min_mentor_comments, + max_comments_eval, + heavily_involved_cutoff, search_query, ) diff --git a/issue_metrics.py b/issue_metrics.py index bd4d040e..f5c750ae 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -31,6 +31,7 @@ from json_writer import write_to_json from labels import get_label_metrics, get_stats_time_in_labels from markdown_writer import write_to_markdown +from most_active_mentors import count_comments_per_user, get_mentor_count from time_to_answer import get_stats_time_to_answer, measure_time_to_answer from time_to_close import get_stats_time_to_close, measure_time_to_close from time_to_first_response import ( @@ -40,8 +41,6 @@ from time_to_merge import measure_time_to_merge from time_to_ready_for_review import get_time_to_ready_for_review -GITHUB_BASE_URL = "https://github.com" - def search_issues( search_query: str, github_connection: github3.GitHub @@ -126,6 +125,8 @@ def get_per_issue_metrics( discussions: bool = False, labels: Union[List[str], None] = None, ignore_users: Union[List[str], None] = None, + max_comments_to_eval: int = 20, + heavily_involved: int = 3, ) -> tuple[List, int, int]: """ Calculate the metrics for each issue/pr/discussion in a list provided. @@ -158,10 +159,20 @@ def get_per_issue_metrics( None, None, None, + None, ) issue_with_metrics.time_to_first_response = measure_time_to_first_response( None, issue, ignore_users ) + issue_with_metrics.mentor_activity = count_comments_per_user( + None, + issue, + ignore_users, + None, + None, + max_comments_to_eval, + heavily_involved, + ) issue_with_metrics.time_to_answer = measure_time_to_answer(issue) if issue["closedAt"]: issue_with_metrics.time_to_close = measure_time_to_close(None, issue) @@ -188,6 +199,15 @@ def get_per_issue_metrics( issue_with_metrics.time_to_first_response = measure_time_to_first_response( issue, None, pull_request, ready_for_review_at, ignore_users ) + issue_with_metrics.mentor_activity = count_comments_per_user( + issue, + None, + pull_request, + ready_for_review_at, + ignore_users, + max_comments_to_eval, + heavily_involved, + ) if labels: issue_with_metrics.label_metrics = get_label_metrics(issue, labels) if issue.state == "closed": # type: ignore @@ -259,6 +279,10 @@ def main(): token, env_vars.ghe, ) + enable_mentor_count = env_vars.enable_mentor_count + min_mentor_count = int(env_vars.min_mentor_comments) + max_comments_eval = int(env_vars.max_comments_eval) + heavily_involved_cutoff = int(env_vars.heavily_involved_cutoff) # Get the repository owner and name from the search query owner = get_owner(search_query) @@ -283,13 +307,13 @@ def main(): issues = get_discussions(token, search_query) if len(issues) <= 0: print("No discussions found") - write_to_markdown(None, None, None, None, None, None, None) + write_to_markdown(None, None, None, None, None, None, None, None) return else: issues = search_issues(search_query, github_connection) if len(issues) <= 0: print("No issues found") - write_to_markdown(None, None, None, None, None, None, None) + write_to_markdown(None, None, None, None, None, None, None, None) return # Get all the metrics @@ -298,6 +322,8 @@ def main(): discussions="type:discussions" in search_query, labels=labels, ignore_users=ignore_users, + max_comments_to_eval=max_comments_eval, + heavily_involved=heavily_involved_cutoff, ) stats_time_to_first_response = get_stats_time_to_first_response(issues_with_metrics) @@ -307,6 +333,10 @@ def main(): stats_time_to_answer = get_stats_time_to_answer(issues_with_metrics) + num_mentor_count = 0 + if enable_mentor_count: + num_mentor_count = get_mentor_count(issues_with_metrics, min_mentor_count) + # Get stats describing the time in label for each label and store it in a dictionary # where the key is the label and the value is the average time stats_time_in_labels = get_stats_time_in_labels(issues_with_metrics, labels) @@ -320,6 +350,7 @@ def main(): stats_time_in_labels, num_issues_open, num_issues_closed, + num_mentor_count, search_query, ) write_to_markdown( @@ -330,6 +361,7 @@ def main(): stats_time_in_labels, num_issues_open, num_issues_closed, + num_mentor_count, labels, search_query, ) diff --git a/json_writer.py b/json_writer.py index 1955ef39..1b40e01c 100644 --- a/json_writer.py +++ b/json_writer.py @@ -30,6 +30,7 @@ def write_to_json( stats_time_in_labels: Union[dict[str, dict[str, timedelta]], None], num_issues_opened: Union[int, None], num_issues_closed: Union[int, None], + num_mentor_count: Union[int, None], search_query: str, ) -> str: """ @@ -42,6 +43,7 @@ def write_to_json( "average_time_to_answer": "1 day, 0:00:00", "num_items_opened": 2, "num_items_closed": 1, + "num_mentor_count": 5, "total_item_count": 2, "issues": [ { @@ -129,6 +131,7 @@ def write_to_json( "90_percentile_time_in_labels": p90_time_in_labels, "num_items_opened": num_issues_opened, "num_items_closed": num_issues_closed, + "num_mentor_count": num_mentor_count, "total_item_count": len(issues_with_metrics), } diff --git a/markdown_writer.py b/markdown_writer.py index ba496229..0b587aef 100644 --- a/markdown_writer.py +++ b/markdown_writer.py @@ -13,6 +13,7 @@ average_time_to_answer: timedelta, num_issues_opened: int, num_issues_closed: int, + num_mentor_count: int, file: file object = None ) -> None: Write the issues with metrics to a markdown file. @@ -79,6 +80,7 @@ def write_to_markdown( average_time_in_labels: Union[dict, None], num_issues_opened: Union[int, None], num_issues_closed: Union[int, None], + num_mentor_count: Union[int, None], labels=None, search_query=None, hide_label_metrics=False, @@ -95,7 +97,8 @@ def write_to_markdown( file (file object, optional): The file object to write to. If not provided, a file named "issue_metrics.md" will be created. num_issues_opened (int): The Number of items that remain opened. - num_issues_closed (int): The number of issues that were closed. + num_issues_closed (int): The number of issues that were closedi. + num_mentor_count (int): The number of very active commentors. labels (List[str]): A list of the labels that are used in the issues. search_query (str): The search query used to find the issues. hide_label_metrics (bool): Represents whether the user has chosen to hide label metrics in the output @@ -127,6 +130,7 @@ def write_to_markdown( average_time_in_labels, num_issues_opened, num_issues_closed, + num_mentor_count, labels, columns, file, @@ -184,6 +188,7 @@ def write_overall_metrics_tables( stats_time_in_labels, num_issues_opened, num_issues_closed, + num_mentor_count, labels, columns, file, @@ -246,4 +251,5 @@ def write_overall_metrics_tables( file.write("| --- | ---: |\n") file.write(f"| Number of items that remain open | {num_issues_opened} |\n") file.write(f"| Number of items closed | {num_issues_closed} |\n") + file.write(f"| Number of most active mentors | {num_mentor_count} |\n") file.write(f"| Total number of items created | {len(issues_with_metrics)} |\n\n") diff --git a/most_active_mentors.py b/most_active_mentors.py new file mode 100755 index 00000000..99bdfaed --- /dev/null +++ b/most_active_mentors.py @@ -0,0 +1,179 @@ +"""A module for measuring the number of very active mentors + +This module provides functions for measuring the number of active mentors on a +project. + +This is measured by number of PR comments. We are working under the assumption +that PR comments are left in good faith to move contributors further instead of +nitpicking and discouraging them. + +Open questions: + - should there be an option to limit this to certain users, e.g. core + maintainers? + - should there be a limit to how many comments per PR we consider to avoid + having the statistic dominated by contested PRs? + - should this metric count consecutive comments coming from the same user as + only one to avoid people unnessesarily splitting their comments to game the + metric? + - instead of PR comments should we count PRs on which a username was seen as + commenter? + +Functions: + collect_response_usernames( + issue: Union[github3.issues.Issue, None], + discussion: Union[dict, None], + pull_request: Union[github3.pulls.PullRequest, None], + max_comments_to_evaluate, + ) -> ____________ + Collect the number of responses per username for single item. Take only + top n comments (max_comments_to_evaluate) into consideration. + get_number_of_active_reviewers( + mentors: List [mentors with metrics) + ) -> int active_number + Count the number of mentors active at least n times + +""" + +from collections import Counter +from datetime import datetime +from typing import Dict, List, Union + +import github3 +from classes import IssueWithMetrics + + +def count_comments_per_user( + issue: Union[github3.issues.Issue, None], # type: ignore + discussion: Union[dict, None] = None, + pull_request: Union[github3.pulls.PullRequest, None] = None, + ready_for_review_at: Union[datetime, None] = None, + ignore_users: List[str] | None = None, + max_comments_to_eval=20, + heavily_involved=3, +) -> dict: + """Count the number of times a user was seen commenting on a single item. + + Args: + issue (Union[github3.issues.Issue, None]): A GitHub issue. + pull_request (Union[github3.pulls.PullRequest, None]): A GitHub pull + request. ignore_users (List[str]): A list of GitHub usernames to + ignore. + max_comments_to_eval: Maximum number of comments per item to look at. + heavily_involved: Maximum number of comments to count for one + user per issue. + + Returns: + dict: A dictionary of usernames seen and number of comments they left. + + """ + if ignore_users is None: + ignore_users = [] + mentor_count: Dict[str, int] = {} + + # Get the first comments + if issue: + comments = issue.issue.comments( + number=max_comments_to_eval, sort="created", direction="asc" + ) # type: ignore + for comment in comments: + if ignore_comment( + issue.issue.user, + comment.user, + ignore_users, + comment.created_at, + ready_for_review_at, + ): + continue + # increase the number of comments left by current user by 1 + if comment.user.login in mentor_count: + if mentor_count[comment.user.login] < heavily_involved: + mentor_count[comment.user.login] += 1 + else: + mentor_count[comment.user.login] = 1 + + # Check if the issue is actually a pull request + # so we may also get the first review comment time + if pull_request: + review_comments = pull_request.reviews(number=max_comments_to_eval) + # type: ignore + for review_comment in review_comments: + if ignore_comment( + issue.issue.user, + review_comment.user, + ignore_users, + review_comment.submitted_at, + ready_for_review_at, + ): + continue + + # increase the number of comments left by current user by 1 + if review_comment.user.login in mentor_count: + mentor_count[review_comment.user.login] += 1 + else: + mentor_count[review_comment.user.login] = 1 + + if discussion and len(discussion["comments"]["nodes"]) > 0: + for comment in discussion["comments"]["nodes"]: + if ignore_comment( + comment.user, + comment.user, + ignore_users, + comment.submitted_at, + comment.ready_for_review_at, + ): + continue + + # increase the number of comments left by current user by 1 + if comment.user.login in mentor_count: + mentor_count[comment.user.login] += 1 + else: + mentor_count[comment.user.login] = 1 + + return mentor_count + + +def ignore_comment( + issue_user: github3.users.User, + comment_user: github3.users.User, + ignore_users: List[str], + comment_created_at: datetime, + ready_for_review_at: Union[datetime, None], +) -> bool: + """Check if a comment should be ignored.""" + return bool( + # ignore comments by IGNORE_USERS + comment_user.login in ignore_users + # ignore comments by bots + or comment_user.type == "Bot" + # ignore comments by the issue creator + or comment_user.login == issue_user.login + # ignore comments created before the issue was ready for review + or (ready_for_review_at and comment_created_at < ready_for_review_at) + ) + + +def get_mentor_count(issues_with_metrics: List[IssueWithMetrics], cutoff: int) -> int: + """Calculate the number of active mentors on the project. + + Args: + issues_with_metrics (List[IssueWithMetrics]): A list of issues w/ + metrics + cutoff (int: the minimum number of comments a user has to leave + to count as active mentor.) + + Returns: + int: Number of active mentors + + """ + + mentor_count: Counter[str] = Counter({}) + for issue_with_metrics in issues_with_metrics: + current_counter = Counter(issue_with_metrics.mentor_activity) + mentor_count = mentor_count + current_counter + + active_mentor_count = 0 + for count in mentor_count.values(): + if count >= cutoff: + active_mentor_count += 1 + + return active_mentor_count diff --git a/test_config.py b/test_config.py index e4a00bb5..d2f1bd15 100644 --- a/test_config.py +++ b/test_config.py @@ -118,6 +118,10 @@ def test_get_env_vars_with_github_app(self): False, [], [], + False, + "10", + "20", + "3", SEARCH_QUERY, ) result = get_env_vars(True) @@ -157,6 +161,10 @@ def test_get_env_vars_with_token(self): False, [], [], + False, + "10", + "20", + "3", SEARCH_QUERY, ) result = get_env_vars(True) @@ -195,6 +203,10 @@ def test_get_env_vars_optional_values(self): True, [], ["waiting-for-review", "waiting-for-manager"], + False, + 10, + 20, + 3, SEARCH_QUERY, ) result = get_env_vars(True) diff --git a/test_issue_metrics.py b/test_issue_metrics.py index 34b2e38a..d192157a 100644 --- a/test_issue_metrics.py +++ b/test_issue_metrics.py @@ -123,7 +123,7 @@ def test_get_env_vars_missing_query(self): # Call the function and check that it raises a ValueError with self.assertRaises(ValueError): - get_env_vars() + get_env_vars(test=True) class TestMain(unittest.TestCase): @@ -231,7 +231,7 @@ def test_main_no_issues_found( # Call main and check that it writes 'No issues found' issue_metrics.main() mock_write_to_markdown.assert_called_once_with( - None, None, None, None, None, None, None + None, None, None, None, None, None, None, None ) diff --git a/test_json_writer.py b/test_json_writer.py index b7b18acd..fe1fe1b8 100644 --- a/test_json_writer.py +++ b/test_json_writer.py @@ -61,6 +61,7 @@ def test_write_to_json(self): } num_issues_opened = 2 num_issues_closed = 1 + num_mentor_count = 5 expected_output = { "average_time_to_first_response": "2 days, 12:00:00", @@ -77,6 +78,7 @@ def test_write_to_json(self): "90_percentile_time_in_labels": {"bug": "1 day, 16:24:12"}, "num_items_opened": 2, "num_items_closed": 1, + "num_mentor_count": 5, "total_item_count": 2, "issues": [ { @@ -111,6 +113,7 @@ def test_write_to_json(self): stats_time_in_labels=stats_time_in_labels, num_issues_opened=num_issues_opened, num_issues_closed=num_issues_closed, + num_mentor_count=num_mentor_count, search_query="is:issue repo:owner/repo", ), json.dumps(expected_output), @@ -149,6 +152,7 @@ def test_write_to_json_with_no_response(self): } num_issues_opened = 2 num_issues_closed = 0 + num_mentor_count = 5 expected_output = { "average_time_to_first_response": "None", @@ -165,6 +169,7 @@ def test_write_to_json_with_no_response(self): "90_percentile_time_in_labels": {}, "num_items_opened": 2, "num_items_closed": 0, + "num_mentor_count": 5, "total_item_count": 2, "issues": [ { @@ -199,6 +204,7 @@ def test_write_to_json_with_no_response(self): stats_time_in_labels=stats_time_in_labels, num_issues_opened=num_issues_opened, num_issues_closed=num_issues_closed, + num_mentor_count=num_mentor_count, search_query="is:issue repo:owner/repo", ), json.dumps(expected_output), diff --git a/test_markdown_writer.py b/test_markdown_writer.py index 00b585cc..053dc0a0 100644 --- a/test_markdown_writer.py +++ b/test_markdown_writer.py @@ -78,6 +78,7 @@ def test_write_to_markdown(self): num_issues_opened = 2 num_issues_closed = 1 + num_mentor_count = 5 # Call the function write_to_markdown( @@ -88,6 +89,7 @@ def test_write_to_markdown(self): average_time_in_labels=time_in_labels, num_issues_opened=num_issues_opened, num_issues_closed=num_issues_closed, + num_mentor_count=num_mentor_count, labels=["bug"], search_query="is:issue is:open label:bug", ) @@ -108,6 +110,7 @@ def test_write_to_markdown(self): "| --- | ---: |\n" "| Number of items that remain open | 2 |\n" "| Number of items closed | 1 |\n" + "| Number of most active mentors | 5 |\n" "| Total number of items created | 2 |\n\n" "| Title | URL | Author | Time to first response | Time to close |" " Time to answer | Time spent in bug |\n" @@ -175,6 +178,7 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): num_issues_opened = 2 num_issues_closed = 1 + num_mentor_count = 5 # Call the function write_to_markdown( @@ -185,6 +189,7 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): average_time_in_labels=average_time_in_labels, num_issues_opened=num_issues_opened, num_issues_closed=num_issues_closed, + num_mentor_count=num_mentor_count, labels=["bug"], ) @@ -204,6 +209,7 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): "| --- | ---: |\n" "| Number of items that remain open | 2 |\n" "| Number of items closed | 1 |\n" + "| Number of most active mentors | 5 |\n" "| Total number of items created | 2 |\n\n" "| Title | URL | Author | Time to first response | Time to close |" " Time to answer | Time spent in bug |\n" @@ -221,7 +227,7 @@ def test_write_to_markdown_no_issues(self): """Test that write_to_markdown writes the correct markdown file when no issues are found.""" # Call the function with no issues with patch("builtins.open", mock_open()) as mock_open_file: - write_to_markdown(None, None, None, None, None, None, None) + write_to_markdown(None, None, None, None, None, None, None, None) # Check that the file was written correctly expected_output = [ @@ -292,6 +298,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): } num_issues_opened = 2 num_issues_closed = 1 + num_mentor_count = 5 # Call the function write_to_markdown( @@ -302,6 +309,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): average_time_in_labels=average_time_in_labels, num_issues_opened=num_issues_opened, num_issues_closed=num_issues_closed, + num_mentor_count=num_mentor_count, labels=["label1"], search_query="repo:user/repo is:issue", hide_label_metrics=True, @@ -316,6 +324,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): "| --- | ---: |\n" "| Number of items that remain open | 2 |\n" "| Number of items closed | 1 |\n" + "| Number of most active mentors | 5 |\n" "| Total number of items created | 2 |\n\n" "| Title | URL | Author |\n" "| --- | --- | --- |\n" diff --git a/test_most_active_mentors.py b/test_most_active_mentors.py new file mode 100755 index 00000000..9fc5db52 --- /dev/null +++ b/test_most_active_mentors.py @@ -0,0 +1,82 @@ +"""A module containing unit tests for the most_active_mentors module. + +This module contains unit tests for the count_comments_per_user and +get_mentor_count functions in the most_active_mentors module. +The tests use mock GitHub issues and comments to test the functions' behavior. + +Classes: + TestCountCommentsPerUser: A class testing count_comments_per_user. + TestGetMentorCount: A class to test the + get_mentor_count function. + +""" + +import unittest +from datetime import datetime +from unittest.mock import MagicMock + +from classes import IssueWithMetrics +from most_active_mentors import count_comments_per_user, get_mentor_count + + +class TestCountCommentsPerUser(unittest.TestCase): + """Test the count_comments_per_user function.""" + + def test_count_comments_per_user(self): + """Test that count_comments_per_user correctly counts user comments. + + This test mocks the GitHub connection and issue comments, and checks + that count_comments_per_user correctly considers user comments for + counting. + + """ + # Set up the mock GitHub issues + mock_issue1 = MagicMock() + mock_issue1.comments = 2 + mock_issue1.issue.user.login = "issue_owner" + mock_issue1.created_at = "2023-01-01T00:00:00Z" + + # Set up 21 mock GitHub issue comments - only 20 should be counted + mock_issue1.issue.comments.return_value = [] + for i in range(22): + mock_comment1 = MagicMock() + mock_comment1.user.login = "very_active_user" + mock_comment1.created_at = datetime.fromisoformat( + f"2023-01-02T{i:02d}:00:00Z" + ) + # pylint: disable=maybe-no-member + mock_issue1.issue.comments.return_value.append(mock_comment1) + + # Call the function + result = count_comments_per_user(mock_issue1) + expected_result = {"very_active_user": 3} + + # Check the results + self.assertEqual(result, expected_result) + + def test_get_mentor_count(self): + """Test that get_mentor_count correctly counts comments per user.""" + mentor_activity = {"sue": 15, "bob": 10} + + # Create mock data + issues_with_metrics = [ + IssueWithMetrics( + "Issue 1", + "https://github.com/user/repo/issues/1", + "alice", + None, + mentor_activity=mentor_activity, + ), + IssueWithMetrics( + "Issue 2", + "https://github.com/user/repo/issues/2", + "bob", + None, + mentor_activity=mentor_activity, + ), + ] + + # Call the function and check the result + result = get_mentor_count(issues_with_metrics, 2) + expected_result = 2 + self.assertEqual(result, expected_result) From 4f29f34d9d831fe224cbc6c8a0d711415ebd01b1 Mon Sep 17 00:00:00 2001 From: lawang24 Date: Sun, 14 Apr 2024 01:55:56 -0400 Subject: [PATCH 029/358] Handle Pending Review Case (#236) --- most_active_mentors.py | 2 ++ test_time_to_first_response.py | 30 ++++++++++++++++++++++++++++++ time_to_first_response.py | 4 +++- 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/most_active_mentors.py b/most_active_mentors.py index 99bdfaed..be0483e3 100755 --- a/most_active_mentors.py +++ b/most_active_mentors.py @@ -147,6 +147,8 @@ def ignore_comment( or comment_user.type == "Bot" # ignore comments by the issue creator or comment_user.login == issue_user.login + # ignore pending reviews + or not comment_created_at # ignore comments created before the issue was ready for review or (ready_for_review_at and comment_created_at < ready_for_review_at) ) diff --git a/test_time_to_first_response.py b/test_time_to_first_response.py index 437a0a9a..a8e592a0 100644 --- a/test_time_to_first_response.py +++ b/test_time_to_first_response.py @@ -233,6 +233,36 @@ def test_measure_time_to_first_response_ignore_users(self): # Check the results self.assertEqual(result, expected_result) + def test_measure_time_to_first_response_ignore_pending_review(self): + """Test that measure_time_to_first_response ignores pending reviews""" + + mock_issue1 = MagicMock() + mock_issue1.comments = 2 + mock_issue1.issue.user.login = "issue_owner" + mock_issue1.created_at = "2023-01-01T00:00:00Z" + + # Set up the mock GitHub pull request comments (one ignored, one not ignored) + # Pending Review + mock_pr_comment1 = MagicMock() + mock_pr_comment1.submitted_at = None + # Submitted Comment + mock_pr_comment2 = MagicMock() + mock_pr_comment2.submitted_at = datetime.fromisoformat("2023-01-04T00:00:00Z") + + mock_pull_request = MagicMock() + mock_pull_request.reviews.return_value = [mock_pr_comment1, mock_pr_comment2] + + ready_for_review_at = datetime.fromisoformat("2023-01-03T00:00:00Z") + + # Call the function + result = measure_time_to_first_response( + mock_issue1, None, mock_pull_request, ready_for_review_at + ) + expected_result = timedelta(days=1) + + # Check the results + self.assertEqual(result, expected_result) + def test_measure_time_to_first_response_only_ignored_users(self): """Test that measure_time_to_first_response returns empty for an issue with only ignored users.""" # Set up the mock GitHub issues diff --git a/time_to_first_response.py b/time_to_first_response.py index 46dc3b89..1f2dc1a1 100644 --- a/time_to_first_response.py +++ b/time_to_first_response.py @@ -129,7 +129,8 @@ def ignore_comment( user_is_a_bot: bool = str(comment_user.type.lower()) == "bot" user_is_issue_creator: bool = str(comment_user.login) == str(issue_user.login) issue_was_created_before_ready_for_review: bool = False - if ready_for_review_at: + is_pending_comment: bool = not isinstance(comment_created_at, datetime) + if ready_for_review_at and not is_pending_comment: issue_was_created_before_ready_for_review = ( comment_created_at < ready_for_review_at ) @@ -137,6 +138,7 @@ def ignore_comment( user_is_ignored or user_is_a_bot or user_is_issue_creator + or is_pending_comment or issue_was_created_before_ready_for_review ) return result From 160cdc1cba841df6e0532fe1fb4dd0438d255de2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Apr 2024 15:28:36 -0500 Subject: [PATCH 030/358] build(deps): bump black from 24.3.0 to 24.4.0 (#238) Bumps [black](https://github.com/psf/black) from 24.3.0 to 24.4.0. - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/compare/24.3.0...24.4.0) --- updated-dependencies: - dependency-name: black dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index f9457d71..5b41c807 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,4 +1,4 @@ -black==24.3.0 +black==24.4.0 flake8==7.0.0 mypy==1.9.0 mypy-extensions==1.0.0 From 217666bc28ebe58a42b8ecbe023624f95fe4ae4a Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Mon, 15 Apr 2024 17:06:18 -0700 Subject: [PATCH 031/358] fix: Performance fix to only measure statistics which are not hidden fixes #177 Signed-off-by: Zack Koppert --- issue_metrics.py | 102 +++++++++++++++++++++++++++-------------------- 1 file changed, 58 insertions(+), 44 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index f5c750ae..628f7f37 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -26,7 +26,7 @@ import github3 from classes import IssueWithMetrics -from config import get_env_vars +from config import EnvVars, get_env_vars from discussions import get_discussions from json_writer import write_to_json from labels import get_label_metrics, get_stats_time_in_labels @@ -127,6 +127,7 @@ def get_per_issue_metrics( ignore_users: Union[List[str], None] = None, max_comments_to_eval: int = 20, heavily_involved: int = 3, + env_vars: EnvVars = None, ) -> tuple[List, int, int]: """ Calculate the metrics for each issue/pr/discussion in a list provided. @@ -138,6 +139,7 @@ def get_per_issue_metrics( Defaults to False. labels (List[str]): A list of labels to measure time spent in. Defaults to empty list. ignore_users (List[str]): A list of users to ignore when calculating metrics. + env_vars (EnvVars): The environment variables for the script. Returns: tuple[List[IssueWithMetrics], int, int]: A tuple containing a @@ -161,24 +163,30 @@ def get_per_issue_metrics( None, None, ) - issue_with_metrics.time_to_first_response = measure_time_to_first_response( - None, issue, ignore_users - ) - issue_with_metrics.mentor_activity = count_comments_per_user( - None, - issue, - ignore_users, - None, - None, - max_comments_to_eval, - heavily_involved, - ) - issue_with_metrics.time_to_answer = measure_time_to_answer(issue) - if issue["closedAt"]: - issue_with_metrics.time_to_close = measure_time_to_close(None, issue) - num_issues_closed += 1 - else: - num_issues_open += 1 + if env_vars.hide_time_to_first_response is False: + issue_with_metrics.time_to_first_response = ( + measure_time_to_first_response(None, issue, ignore_users) + ) + if env_vars.enable_mentor_count: + issue_with_metrics.mentor_activity = count_comments_per_user( + None, + issue, + ignore_users, + None, + None, + max_comments_to_eval, + heavily_involved, + ) + if env_vars.hide_time_to_answer is False: + issue_with_metrics.time_to_answer = measure_time_to_answer(issue) + if env_vars.hide_time_to_close is False: + if issue["closedAt"]: + issue_with_metrics.time_to_close = measure_time_to_close( + None, issue + ) + num_issues_closed += 1 + else: + num_issues_open += 1 else: issue_with_metrics = IssueWithMetrics( issue.title, # type: ignore @@ -196,32 +204,37 @@ def get_per_issue_metrics( pull_request = issue.issue.pull_request() # type: ignore ready_for_review_at = get_time_to_ready_for_review(issue, pull_request) - issue_with_metrics.time_to_first_response = measure_time_to_first_response( - issue, None, pull_request, ready_for_review_at, ignore_users - ) - issue_with_metrics.mentor_activity = count_comments_per_user( - issue, - None, - pull_request, - ready_for_review_at, - ignore_users, - max_comments_to_eval, - heavily_involved, - ) - if labels: - issue_with_metrics.label_metrics = get_label_metrics(issue, labels) - if issue.state == "closed": # type: ignore - if pull_request: - issue_with_metrics.time_to_close = measure_time_to_merge( - pull_request, ready_for_review_at + if env_vars.hide_time_to_first_response is False: + issue_with_metrics.time_to_first_response = ( + measure_time_to_first_response( + issue, None, pull_request, ready_for_review_at, ignore_users ) - else: - issue_with_metrics.time_to_close = measure_time_to_close( - issue, None - ) - num_issues_closed += 1 - elif issue.state == "open": # type: ignore - num_issues_open += 1 + ) + if env_vars.enable_mentor_count: + issue_with_metrics.mentor_activity = count_comments_per_user( + issue, + None, + pull_request, + ready_for_review_at, + ignore_users, + max_comments_to_eval, + heavily_involved, + ) + if labels and env_vars.hide_label_metrics is False: + issue_with_metrics.label_metrics = get_label_metrics(issue, labels) + if env_vars.hide_time_to_close is False: + if issue.state == "closed": # type: ignore + if pull_request: + issue_with_metrics.time_to_close = measure_time_to_merge( + pull_request, ready_for_review_at + ) + else: + issue_with_metrics.time_to_close = measure_time_to_close( + issue, None + ) + num_issues_closed += 1 + elif issue.state == "open": # type: ignore + num_issues_open += 1 issues_with_metrics.append(issue_with_metrics) return issues_with_metrics, num_issues_open, num_issues_closed @@ -324,6 +337,7 @@ def main(): ignore_users=ignore_users, max_comments_to_eval=max_comments_eval, heavily_involved=heavily_involved_cutoff, + env_vars=env_vars, ) stats_time_to_first_response = get_stats_time_to_first_response(issues_with_metrics) From 2b4cce2ba4aab2ffc5945d9505902c7f4ec24081 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 16 Apr 2024 10:53:48 -0700 Subject: [PATCH 032/358] fix: Set proper default. Co-authored-by: jmeridth Signed-off-by: Zack Koppert --- issue_metrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/issue_metrics.py b/issue_metrics.py index 628f7f37..85c3b39e 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -127,7 +127,7 @@ def get_per_issue_metrics( ignore_users: Union[List[str], None] = None, max_comments_to_eval: int = 20, heavily_involved: int = 3, - env_vars: EnvVars = None, + env_vars: EnvVars = get_env_vars(), ) -> tuple[List, int, int]: """ Calculate the metrics for each issue/pr/discussion in a list provided. From 0856d7db6d57887102385192c4aef112bca76e16 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 16 Apr 2024 14:26:16 -0700 Subject: [PATCH 033/358] set test bool when using it in tests Signed-off-by: Zack Koppert --- issue_metrics.py | 2 +- test_issue_metrics.py | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index 85c3b39e..15f58ee6 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -122,12 +122,12 @@ def auth_to_github( def get_per_issue_metrics( issues: Union[List[dict], List[github3.search.IssueSearchResult]], # type: ignore + env_vars: EnvVars, discussions: bool = False, labels: Union[List[str], None] = None, ignore_users: Union[List[str], None] = None, max_comments_to_eval: int = 20, heavily_involved: int = 3, - env_vars: EnvVars = get_env_vars(), ) -> tuple[List, int, int]: """ Calculate the metrics for each issue/pr/discussion in a list provided. diff --git a/test_issue_metrics.py b/test_issue_metrics.py index d192157a..582dc2e6 100644 --- a/test_issue_metrics.py +++ b/test_issue_metrics.py @@ -108,8 +108,8 @@ def test_get_env_vars(self): """Test that the function correctly retrieves the environment variables.""" # Call the function and check the result - search_query = get_env_vars().search_query - gh_token = get_env_vars().gh_token + search_query = get_env_vars(test=True).search_query + gh_token = get_env_vars(test=True).gh_token gh_token_expected_result = "test_token" search_query_expected_result = "is:issue is:open repo:user/repo" self.assertEqual(gh_token, gh_token_expected_result) @@ -238,6 +238,10 @@ def test_main_no_issues_found( class TestGetPerIssueMetrics(unittest.TestCase): """Test suite for the get_per_issue_metrics function.""" + @patch.dict( + os.environ, + {"GH_TOKEN": "test_token", "SEARCH_QUERY": "is:issue is:open repo:user/repo"}, + ) def test_get_per_issue_metrics(self): """Test that the function correctly calculates the metrics for a list of GitHub issues.""" # Create mock data @@ -286,7 +290,7 @@ def test_get_per_issue_metrics(self): result_issues_with_metrics, result_num_issues_open, result_num_issues_closed, - ) = get_per_issue_metrics(issues) + ) = get_per_issue_metrics(issues, env_vars=get_env_vars(test=True)) expected_issues_with_metrics = [ IssueWithMetrics( "Issue 1", @@ -364,6 +368,10 @@ def setUp(self): "closedAt": "2023-01-07T00:00:00Z", } + @patch.dict( + os.environ, + {"GH_TOKEN": "test_token", "SEARCH_QUERY": "is:issue is:open repo:user/repo"}, + ) def test_get_per_issue_metrics_with_discussion(self): """ Test that the function correctly calculates @@ -371,7 +379,9 @@ def test_get_per_issue_metrics_with_discussion(self): """ issues = [self.issue1, self.issue2] - metrics = get_per_issue_metrics(issues, discussions=True) + metrics = get_per_issue_metrics( + issues, discussions=True, env_vars=get_env_vars(test=True) + ) # get_per_issue_metrics returns a tuple of # (issues_with_metrics, num_issues_open, num_issues_closed) From f76ff469d8d748b240b70f6e4cf4a9fdcdd3f6d0 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 16 Apr 2024 15:37:48 -0700 Subject: [PATCH 034/358] test: ensure via testing all test*.py files call get_env_vars() with test=True Signed-off-by: Zack Koppert --- .github/scripts/env_vars_check.sh | 22 ++++++++++++++++++++++ Makefile | 1 + 2 files changed, 23 insertions(+) create mode 100755 .github/scripts/env_vars_check.sh diff --git a/.github/scripts/env_vars_check.sh b/.github/scripts/env_vars_check.sh new file mode 100755 index 00000000..6b6eeb6d --- /dev/null +++ b/.github/scripts/env_vars_check.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# Find all test_*.py files +files=$(find . -name "test_*.py") +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' # No Color + +# Loop through each file +for file in $files; do + # Search for instances of get_env_vars() with no arguments + result=$(grep -n "get_env_vars()" "$file") + + # If any instances are found, print the file name and line number + if [ -n "$result" ]; then + echo "Found in $file:" + echo "$result" + echo -e "${RED}ERROR: get_env_vars() should always set test=True in test*.py files.${NC}" + exit 1 + fi +done +echo -e " ${GREEN}PASS:${NC} All test*.py files call get_env_vars() with test=True." \ No newline at end of file diff --git a/Makefile b/Makefile index 58d74793..c94fa87c 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ .PHONY: test test: pytest -v --cov=. --cov-config=.coveragerc --cov-fail-under=80 --cov-report term-missing + .github/scripts/env_vars_check.sh .PHONY: clean clean: From 33529fcba000b35c512daf5938cdc94790b9d502 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 16 Apr 2024 16:03:02 -0700 Subject: [PATCH 035/358] chore: add newline at EOF Signed-off-by: Zack Koppert --- .github/scripts/env_vars_check.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/env_vars_check.sh b/.github/scripts/env_vars_check.sh index 6b6eeb6d..2d3fb018 100755 --- a/.github/scripts/env_vars_check.sh +++ b/.github/scripts/env_vars_check.sh @@ -19,4 +19,4 @@ for file in $files; do exit 1 fi done -echo -e " ${GREEN}PASS:${NC} All test*.py files call get_env_vars() with test=True." \ No newline at end of file +echo -e " ${GREEN}PASS:${NC} All test*.py files call get_env_vars() with test=True." From bef68c028f4235457684fbfc35e50c13a241e23b Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 16 Apr 2024 16:03:53 -0700 Subject: [PATCH 036/358] chore: more spaces means more better Signed-off-by: Zack Koppert --- .github/scripts/env_vars_check.sh | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/scripts/env_vars_check.sh b/.github/scripts/env_vars_check.sh index 2d3fb018..779ce038 100755 --- a/.github/scripts/env_vars_check.sh +++ b/.github/scripts/env_vars_check.sh @@ -8,15 +8,15 @@ NC='\033[0m' # No Color # Loop through each file for file in $files; do - # Search for instances of get_env_vars() with no arguments - result=$(grep -n "get_env_vars()" "$file") + # Search for instances of get_env_vars() with no arguments + result=$(grep -n "get_env_vars()" "$file") - # If any instances are found, print the file name and line number - if [ -n "$result" ]; then - echo "Found in $file:" - echo "$result" - echo -e "${RED}ERROR: get_env_vars() should always set test=True in test*.py files.${NC}" - exit 1 - fi + # If any instances are found, print the file name and line number + if [ -n "$result" ]; then + echo "Found in $file:" + echo "$result" + echo -e "${RED}ERROR: get_env_vars() should always set test=True in test*.py files.${NC}" + exit 1 + fi done echo -e " ${GREEN}PASS:${NC} All test*.py files call get_env_vars() with test=True." From 2cdc7eac312a2fe236695842570f493f7b4f5c5d Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 16 Apr 2024 16:09:28 -0700 Subject: [PATCH 037/358] chore: use tabs in bash scripts Signed-off-by: Zack Koppert --- .github/scripts/env_vars_check.sh | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/scripts/env_vars_check.sh b/.github/scripts/env_vars_check.sh index 779ce038..ecee7bed 100755 --- a/.github/scripts/env_vars_check.sh +++ b/.github/scripts/env_vars_check.sh @@ -8,15 +8,15 @@ NC='\033[0m' # No Color # Loop through each file for file in $files; do - # Search for instances of get_env_vars() with no arguments - result=$(grep -n "get_env_vars()" "$file") + # Search for instances of get_env_vars() with no arguments + result=$(grep -n "get_env_vars()" "$file") - # If any instances are found, print the file name and line number - if [ -n "$result" ]; then - echo "Found in $file:" - echo "$result" - echo -e "${RED}ERROR: get_env_vars() should always set test=True in test*.py files.${NC}" - exit 1 - fi + # If any instances are found, print the file name and line number + if [ -n "$result" ]; then + echo "Found in $file:" + echo "$result" + echo -e "${RED}ERROR: get_env_vars() should always set test=True in test*.py files.${NC}" + exit 1 + fi done echo -e " ${GREEN}PASS:${NC} All test*.py files call get_env_vars() with test=True." From b6893aceb0df0c4fe6ccdca84043049cfc9adf94 Mon Sep 17 00:00:00 2001 From: Jason Meridth Date: Wed, 17 Apr 2024 23:55:32 -0500 Subject: [PATCH 038/358] fix: add config for jscpd linter (#241) --- .github/linters/.jscpd.json | 26 ++++++++++++++++++++++++++ .gitignore | 3 +++ 2 files changed, 29 insertions(+) create mode 100644 .github/linters/.jscpd.json diff --git a/.github/linters/.jscpd.json b/.github/linters/.jscpd.json new file mode 100644 index 00000000..475c2ca1 --- /dev/null +++ b/.github/linters/.jscpd.json @@ -0,0 +1,26 @@ +{ + "exitCode": 0, + "ignore": [ + "**/.venv/**", + "**/.coverage*", + "**/.devcontainer/**", + "**/.git/**", + "**/.gitignore", + "**/.github/**", + "**/.mypy_cache/**", + "**/.pytest_cache/**", + "**/__pycache__/**", + "**/build/**", + "**/dist/**", + "**/docs/**", + "**/migrations/**", + "**/node_modules/**", + "**/report/**", + "**/test_*.py", + "**/venv/**", + "**/*.md", + "**/Dockerfile", + "**/LICENSE", + "**/Makefile" + ] +} diff --git a/.gitignore b/.gitignore index 0f01f758..5aab7254 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ __pycache__/ # C extensions *.so +# JSCPD +report/ + # Distribution / packaging .Python build/ From 4dd12ebe6d6a3e9bd89aa50dd337990f11ad4f9a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:15:30 -0500 Subject: [PATCH 039/358] build(deps): bump types-pytz from 2024.1.0.20240203 to 2024.1.0.20240417 (#240) Bumps [types-pytz](https://github.com/python/typeshed) from 2024.1.0.20240203 to 2024.1.0.20240417. - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-pytz dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 5b41c807..e768f26d 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -5,5 +5,5 @@ mypy-extensions==1.0.0 pylint==3.1.0 pytest==8.1.1 pytest-cov==5.0.0 -types-pytz==2024.1.0.20240203 +types-pytz==2024.1.0.20240417 types-requests==2.31.0.20240406 From d33a996fcdcfdf0f940574e7bffa00079498ff28 Mon Sep 17 00:00:00 2001 From: Jason Meridth Date: Fri, 19 Apr 2024 13:35:08 -0500 Subject: [PATCH 040/358] feat: automated releases and auto labelling (#242) - [x] lint pr title adhering to conventional commits (helps with auto labelling) - [x] Update CONTRIBUTING.md - [x] automated releases - [x] how to file a bug report - [x] how to file an enhancement - [x] Update pull_request_template with new standard - [x] setup autolabelling through release-drafter github action config - [x] create auto-labeler workflow - [x] change release-drafter workflow to release - [x] create release, including new tag - [x] create discussion announcement based on release - [x] build container images, tag them and release them Signed-off-by: jmeridth --- .github/pull_request-template.md | 11 +++- .github/release-drafter.yml | 37 ++++++++++-- .github/workflows/auto-labeler.yml | 28 +++++++++ .github/workflows/pr-title.yml | 41 ++++++++++++++ .github/workflows/release-drafter.yml | 22 ------- .github/workflows/release.yml | 82 +++++++++++++++++++++++++++ CONTRIBUTING.md | 37 ++---------- 7 files changed, 198 insertions(+), 60 deletions(-) create mode 100644 .github/workflows/auto-labeler.yml create mode 100644 .github/workflows/pr-title.yml delete mode 100644 .github/workflows/release-drafter.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/pull_request-template.md b/.github/pull_request-template.md index beb28c0a..a2b7f41c 100644 --- a/.github/pull_request-template.md +++ b/.github/pull_request-template.md @@ -1,5 +1,12 @@ # Pull Request - + ## Proposed Changes @@ -14,4 +21,4 @@ ### Reviewer -- [ ] Label as either `bug`, `documentation`, `enhancement`, `infrastructure`, or `breaking` +- [ ] Label as either `fix`, `documentation`, `enhancement`, `infrastructure`, or `breaking` diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 24300e17..a07d612d 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -4,9 +4,9 @@ tag-template: 'v$RESOLVED_VERSION' template: | # Changelog $CHANGES - - See details of [all code changes](https://github.com/github/issue-metrics/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION) since previous release - + + See details of [all code changes](https://github.com/github/issue-metrics/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION) since previous release + categories: - title: '🚀 Features' labels: @@ -23,6 +23,8 @@ categories: - 'automation' - 'documentation' - 'dependencies' + - 'maintenance' + - 'revert' - title: '🏎 Performance' label: 'performance' change-template: '- $TITLE @$AUTHOR (#$NUMBER)' @@ -35,7 +37,32 @@ version-resolver: - 'enhancement' patch: labels: - - 'bug' - - 'maintenance' + - 'fix' - 'documentation' + - 'maintenance' default: patch +autolabeler: + - label: 'automation' + title: + - '/^(build|ci|perf|refactor|test).*/i' + - label: 'enhancement' + title: + - '/^(style).*/i' + - label: 'documentation' + title: + - '/^(docs).*/i' + - label: 'feature' + title: + - '/^(feat).*/i' + - label: 'fix' + title: + - '/^(fix).*/i' + - label: 'infrastructure' + title: + - '/^(infrastructure).*/i' + - label: 'maintenance' + title: + - '/^(chore|maintenance).*/i' + - label: 'revert' + title: + - '/^(revert).*/i' diff --git a/.github/workflows/auto-labeler.yml b/.github/workflows/auto-labeler.yml new file mode 100644 index 00000000..a13ee68c --- /dev/null +++ b/.github/workflows/auto-labeler.yml @@ -0,0 +1,28 @@ +--- + name: Auto Labeler + + on: + # pull_request event is required only for autolabeler + pull_request: + # Only following types are handled by the action, but one can default to all as well + types: [opened, reopened, synchronize] + # pull_request_target event is required for autolabeler to support PRs from forks + pull_request_target: + types: [opened, reopened, synchronize] + + permissions: + contents: read + + jobs: + main: + permissions: + contents: write + pull-requests: write + name: Auto label pull requests + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@v6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + config-name: release-drafter.yml diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml new file mode 100644 index 00000000..de39d118 --- /dev/null +++ b/.github/workflows/pr-title.yml @@ -0,0 +1,41 @@ +## Reference: https://github.com/amannn/action-semantic-pull-request +--- +name: "Lint PR Title" + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +permissions: + contents: read + +jobs: + main: + permissions: + pull-requests: read + statuses: write + name: Validate PR title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + # Configure which types are allowed (newline-delimited). + # From: https://github.com/commitizen/conventional-commit-types/blob/master/index.json + # listing all below + types: | + build + chore + ci + docs + feat + fix + perf + refactor + revert + style + test diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml deleted file mode 100644 index c987bebb..00000000 --- a/.github/workflows/release-drafter.yml +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: Release Drafter - -on: - push: - # branches to consider in the event; optional, defaults to all - branches: - - main - -permissions: - contents: read - -jobs: - update_release_draft: - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - steps: - - uses: release-drafter/release-drafter@v6 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..24cdbb93 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,82 @@ +--- + name: Release + + on: + workflow_dispatch: + push: + branches: + - main + + permissions: + contents: read + + jobs: + create_release: + outputs: + full-tag: ${{ steps.release-drafter.outputs.tag_name }} + short-tag: ${{ steps.get_tag_name.outputs.SHORT_TAG }} + body: ${{ steps.release-drafter.outputs.body }} + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: read + steps: + - uses: release-drafter/release-drafter@v6 + id: release-drafter + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + config-name: release-drafter.yml + publish: true + - name: Get the short tag + id: get_tag_name + run: | + short_tag=$(echo ${{ steps.release-drafter.outputs.tag_name }} | cut -d. -f1) + echo "SHORT_TAG=$short_tag" >> $GITHUB_OUTPUT + create_action_images: + needs: create_release + runs-on: ubuntu-latest + permissions: + packages: write + env: + REGISTRY: ghcr.io + IMAGE_NAME: issue_metrics # different than repo name (underscore instead of dash) + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/checkout@v4 + - name: Push Docker Image + if: ${{ success() }} + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.create_release.outputs.full-tag }} + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.create_release.outputs.short-tag }} + platforms: linux/amd64 + provenance: false + sbom: false + create_discussion: + needs: create_release + runs-on: ubuntu-latest + permissions: + discussions: write + steps: + - name: Create an announcement discussion for release + uses: abirismyname/create-discussion@v1.2.0 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + title: ${{ needs.create_release.outputs.full-tag }} + body: ${{ needs.create_release.outputs.body }} + repository-id: ${{ secrets.RELEASE_DISCUSSION_REPOSITORY_ID }} + category-id: ${{ secrets.RELEASE_DISCUSSION_CATEGORY_ID }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 08770fe1..e79d3e93 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,18 +52,7 @@ A good bug report shouldn't leave others needing to chase you up for more inform ### How Do I Submit a Good Bug Report? -We use GitHub issues to track bugs and errors. If you run into an issue with the project: - -- Open an [Issue](https://github.com/github/issue-metrics/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.) -- Explain the behavior you would expect and the actual behavior. -- Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case. -- Provide the information you collected in the previous section. - -Once it's filed: - -- The project team will label the issue accordingly. -- A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced. -- If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be implemented by someone. +Please submit a bug report using our [GitHub Issues template](https://github.com/github/issue-metrics/issues/new?template=bug_report.yml). ## Suggesting Enhancements @@ -80,26 +69,12 @@ This section guides you through submitting an enhancement suggestion for issue-m ### How Do I Submit a Good Enhancement Suggestion? -Enhancement suggestions are tracked as [GitHub issues](https://github.com/github/issue-metrics/issues). +Please submit an enhancement suggestion using our [GitHub Issues template](https://github.com/github/issue-metrics/issues/new?template=feature_request.yml). + +### Pull Request Standards -- Use a **clear and descriptive title** for the issue to identify the suggestion. -- Provide a **step-by-step description of the suggested enhancement** in as many details as possible. -- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you. -- You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. -- **Explain why this enhancement would be useful** to most issue-metrics users. +We are using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) to standardize our pull request titles. This allows us to automatically generate labels and changelogs and follow semantic versioning. Please follow the commit message format when creating a pull request. What pull request title prefixes are expected are in the [pull_request_template.md](.github/pull_request_template.md) that is shown when creating a pull request. ## Releases -To release a new version, maintainers are to release new versions following semantic versioning and via GitHub Releases. -Once the code is ready to release please do the following -1. Create a [GitHub release](https://github.com/github/issue-metrics/releases) based off the current draft and review release notes -2. Ensure that the versioning is correct given the content of the release -3. Check the box to release it to the GitHub Marketplace -4. Publish the release -5. Clone the repository at the release tag locally or in a codespace -6. Authenticate to ghcr.io using [these instructions](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry) -7. `docker build -t ghcr.io/github/issue-metrics:v1 .` where v1 is the current major version number -8. `docker build -t ghcr.io/github/issue-metrics:v1.0.0 .` where v1.0.0 is the full version number -9. `docker push ghcr.io/github/issue-metrics:v1` where v1 is the current major version number -10. `docker push ghcr.io/github/issue-metrics:v1.0.0` where v1.0.0 is the full version number -11. Update the `action.yml` and `README.md` instructions to point to the new docker container if its a major version number change +Releases are automated but if you need to manually initiate a release you can do so through the GitHub Actions UI. If you have permissions to do so, you can navigate to the [Actions tab](https://github.com/github/issue-metrics/actions/workflows/release.yml) and select the `Run workflow` button. This will allow you to select the branch to release from and the version to release. From b39021b5d3b87e6572da0f7470ab973411f546c5 Mon Sep 17 00:00:00 2001 From: Jason Meridth Date: Sat, 20 Apr 2024 19:11:28 -0500 Subject: [PATCH 041/358] fix: release container image build (#244) Seems the [`Unexpected status from HEAD request to : 400 Bad Request`](https://github.com/github/issue-metrics/actions/runs/8758107284/job/24038180863#step:5:1024) is an intermittent error in GitHub Actions and pushing to ghcr that can be solved by using moby/buildkit via https://github.com/docker/build-push-action/issues/761#issuecomment-1383822381 Going to try this with latest release of moby/buildkit to see if it fixes it Signed-off-by: jmeridth --- .github/workflows/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 24cdbb93..cb9cf949 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,6 +44,9 @@ steps: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + with: + driver-opts: | + image=moby/buildkit:v0.13.1 - name: Log in to the Container registry uses: docker/login-action@v3 with: From e7731ae724cd13b8449e421872ac359942cf4fcf Mon Sep 17 00:00:00 2001 From: Jason Meridth Date: Mon, 22 Apr 2024 17:47:46 -0500 Subject: [PATCH 042/358] fix: container image name (#247) - [x] needs to be owner/repo, not just repo (excellent catch @zkoppert, thank you) - [x] remove unnecessary mobykit versioning Signed-off-by: jmeridth Co-authored-by: Zack Koppert --- .../{pull_request-template.md => pull_request_template.md} | 0 .github/workflows/release.yml | 5 +---- 2 files changed, 1 insertion(+), 4 deletions(-) rename .github/{pull_request-template.md => pull_request_template.md} (100%) diff --git a/.github/pull_request-template.md b/.github/pull_request_template.md similarity index 100% rename from .github/pull_request-template.md rename to .github/pull_request_template.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cb9cf949..1ae95f5c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,13 +40,10 @@ packages: write env: REGISTRY: ghcr.io - IMAGE_NAME: issue_metrics # different than repo name (underscore instead of dash) + IMAGE_NAME: github/issue_metrics # different than repo name (underscore instead of dash) steps: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - with: - driver-opts: | - image=moby/buildkit:v0.13.1 - name: Log in to the Container registry uses: docker/login-action@v3 with: From 95d854c635cba901fbd856eab5f70aa65b87ee11 Mon Sep 17 00:00:00 2001 From: Jason Meridth Date: Tue, 23 Apr 2024 12:49:58 -0500 Subject: [PATCH 043/358] chore: add owner and repository names to errors (#249) Add owner and repository info to error messages Signed-off-by: jmeridth --- issue_metrics.py | 35 +++++++++++++++++++++-------------- test_issue_metrics.py | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 15 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index 15f58ee6..330d79bd 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -43,7 +43,7 @@ def search_issues( - search_query: str, github_connection: github3.GitHub + search_query: str, github_connection: github3.GitHub, owner: str, repository: str ) -> List[github3.search.IssueSearchResult]: # type: ignore """ Searches for issues/prs/discussions in a GitHub repository that match @@ -52,6 +52,8 @@ def search_issues( Args: search_query (str): The search query to use for finding issues/prs/discussions. github_connection (github3.GitHub): A connection to the GitHub API. + owner (str): The owner of the repository to search in. + repository (str): The repository to search in. Returns: List[github3.search.IssueSearchResult]: A list of issues that match the search query. @@ -67,11 +69,13 @@ def search_issues( issues.append(issue) except github3.exceptions.ForbiddenError: print( - "You do not have permission to view this repository; Check you API Token." + f"You do not have permission to view this repository '{repository}'; Check your API Token." ) sys.exit(1) except github3.exceptions.NotFoundError: - print("The repository could not be found; Check the repository owner and name.") + print( + f"The repository could not be found; Check the repository owner and name: '{owner}/{repository}" + ) sys.exit(1) except github3.exceptions.ConnectionError: print( @@ -240,27 +244,28 @@ def get_per_issue_metrics( return issues_with_metrics, num_issues_open, num_issues_closed -def get_owner( +def get_owner_and_repository( search_query: str, -) -> Union[str, None]: - """Get the owner from the search query. +) -> dict: + """Get the owner and repository from the search query. Args: search_query (str): The search query used to search for issues. Returns: - Union[str, None]: The owner. + dict: A dictionary of owner and repository. """ search_query_split = search_query.split(" ") - owner = None + result = {} for item in search_query_split: if "repo:" in item and "/" in item: - owner = item.split(":")[1].split("/")[0] + result["owner"] = item.split(":")[1].split("/")[0] + result["repository"] = item.split(":")[1].split("/")[1] if "org:" in item or "owner:" in item or "user:" in item: - owner = item.split(":")[1] + result["owner"] = item.split(":")[1] - return owner + return result def main(): @@ -297,8 +302,10 @@ def main(): max_comments_eval = int(env_vars.max_comments_eval) heavily_involved_cutoff = int(env_vars.heavily_involved_cutoff) - # Get the repository owner and name from the search query - owner = get_owner(search_query) + # Get the owner and repository from the search query + owner_and_repository = get_owner_and_repository(search_query) + owner = owner_and_repository.get("owner") + repository = owner_and_repository.get("repository") if owner is None: raise ValueError( @@ -323,7 +330,7 @@ def main(): write_to_markdown(None, None, None, None, None, None, None, None) return else: - issues = search_issues(search_query, github_connection) + issues = search_issues(search_query, github_connection, owner, repository) if len(issues) <= 0: print("No issues found") write_to_markdown(None, None, None, None, None, None, None, None) diff --git a/test_issue_metrics.py b/test_issue_metrics.py index 582dc2e6..60a11003 100644 --- a/test_issue_metrics.py +++ b/test_issue_metrics.py @@ -24,6 +24,7 @@ IssueWithMetrics, auth_to_github, get_env_vars, + get_owner_and_repository, get_per_issue_metrics, measure_time_to_close, measure_time_to_first_response, @@ -54,10 +55,45 @@ def test_search_issues(self): mock_connection.search_issues.return_value = mock_issues # Call search_issues and check that it returns the correct issues - issues = search_issues("is:open", mock_connection) + issues = search_issues( + "is:open", mock_connection, "fakeowner", "fakerepository" + ) self.assertEqual(issues, mock_issues) +class TestGetOwnerAndRepository(unittest.TestCase): + """Unit tests for the get_owner_and_repository function. + + This class contains unit tests for the get_owner_and_repository function in the + issue_metrics module. The tests use the unittest module and the unittest.mock + module to mock the GitHub API and test the function in isolation. + + Methods: + test_get_owner_with_owner_and_repo_in_query: Test get both owner and repo. + test_get_owner_and_repository_with_repo_in_query: Test get just owner. + test_get_owner_and_repository_without_either_in_query: Test get neither. + + """ + + def test_get_owner_with_owner_and_repo_in_query(self): + """Test get both owner and repo.""" + result = get_owner_and_repository("repo:owner1/repo1") + self.assertEqual(result.get("owner"), "owner1") + self.assertEqual(result.get("repository"), "repo1") + + def test_get_owner_and_repository_with_repo_in_query(self): + """Test get just owner.""" + result = get_owner_and_repository("org:owner1") + self.assertEqual(result.get("owner"), "owner1") + self.assertIsNone(result.get("repository")) + + def test_get_owner_and_repository_without_either_in_query(self): + """Test get neither.""" + result = get_owner_and_repository("is:blah") + self.assertIsNone(result.get("owner")) + self.assertIsNone(result.get("repository")) + + class TestAuthToGithub(unittest.TestCase): """Test the auth_to_github function.""" From 0b5e8b4da0aaf6b6c4fb63312bf96f0a5d9b1fd5 Mon Sep 17 00:00:00 2001 From: Jason Meridth Date: Wed, 24 Apr 2024 22:23:54 -0500 Subject: [PATCH 044/358] chore(deps): update dependabot config to include pr prefixes (#253) Closes #252 Closes #251 Add commit-message.prefix config option to dependabot so that dependabot pull request titles pass our new PR Title Linting workflow - [x] include current dependency updates (black and mypy) Signed-off-by: jmeridth --- .github/dependabot.yml | 11 ++++++----- requirements-test.txt | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0782e612..06abb029 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,20 +1,21 @@ --- -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates - version: 2 updates: - package-ecosystem: "pip" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "daily" + commit-message: + prefix: "chore(deps)" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" + commit-message: + prefix: "chore(deps)" - package-ecosystem: "docker" directory: "/" schedule: interval: "daily" + commit-message: + prefix: "chore(deps)" diff --git a/requirements-test.txt b/requirements-test.txt index e768f26d..641c7bd1 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,6 +1,6 @@ -black==24.4.0 +black==24.4.1 flake8==7.0.0 -mypy==1.9.0 +mypy==1.10.0 mypy-extensions==1.0.0 pylint==3.1.0 pytest==8.1.1 From fb943469dfdf874406ad14932d7065941ea3355f Mon Sep 17 00:00:00 2001 From: Jason Meridth Date: Mon, 29 Apr 2024 15:45:39 -0500 Subject: [PATCH 045/358] fix: use GitHub App token when authed with GitHub App (#257) * fix: use GitHub App token when authed with GitHub App Currently we are still trying to use the GH_TOKEN env var when making api/graphql calls even after authenticating with a GitHub App Installation. This change fixes that. Also fixed a few other things while in here: - [x] split authentication to auth.py file (like other actions) - [x] fix arguments to the count_comments_per_user function - [x] add `maintenance` to pull request template Signed-off-by: jmeridth * test: Add test to cover ignored users in mentor_count Signed-off-by: Zack Koppert --------- Signed-off-by: jmeridth Signed-off-by: Zack Koppert Co-authored-by: Zack Koppert --- .github/pull_request_template.md | 2 +- auth.py | 63 ++++++++++++++++++++++++++++ issue_metrics.py | 51 +++++++--------------- most_active_mentors.py | 4 +- test_auth.py | 72 ++++++++++++++++++++++++++++++++ test_issue_metrics.py | 42 ------------------- test_most_active_mentors.py | 41 +++++++++++++++++- 7 files changed, 192 insertions(+), 83 deletions(-) create mode 100644 auth.py create mode 100644 test_auth.py diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a2b7f41c..fed940d3 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -21,4 +21,4 @@ examples: "feat: add new logger" or "fix: remove unused imports" ### Reviewer -- [ ] Label as either `fix`, `documentation`, `enhancement`, `infrastructure`, or `breaking` +- [ ] Label as either `fix`, `documentation`, `enhancement`, `infrastructure`, `maintenance`, or `breaking` diff --git a/auth.py b/auth.py new file mode 100644 index 00000000..25a37cb3 --- /dev/null +++ b/auth.py @@ -0,0 +1,63 @@ +""" +This is the module that contains functions related to authenticating +to GitHub. +""" + +import github3 +import requests + + +def auth_to_github( + gh_app_id: str, + gh_app_installation_id: int, + gh_app_private_key_bytes: bytes, + token: str, + ghe: str, +) -> github3.GitHub: + """ + Connect to GitHub.com or GitHub Enterprise, depending on env variables. + + Returns: + github3.GitHub: A github api connection. + """ + + if gh_app_id and gh_app_private_key_bytes and gh_app_installation_id: + gh = github3.github.GitHub() + gh.login_as_app_installation( + gh_app_private_key_bytes, gh_app_id, gh_app_installation_id + ) + github_connection = gh + elif ghe and token: + github_connection = github3.github.GitHubEnterprise(ghe, token=token) + elif token: + github_connection = github3.login(token=token) + else: + raise ValueError( + "GH_TOKEN or the set of [GH_APP_ID, GH_APP_INSTALLATION_ID, GH_APP_PRIVATE_KEY] environment variables are not set" + ) + + return github_connection # type: ignore + + +def get_github_app_installation_token( + gh_app_id: str, gh_app_private_key_bytes: bytes, gh_app_installation_id: str +) -> str | None: + """ + Get a GitHub App Installation token. + Args: + gh_app_id (str): the GitHub App ID + gh_app_private_key_bytes (bytes): the GitHub App Private Key + gh_app_installation_id (str): the GitHub App Installation ID + Returns: + str: the GitHub App token + """ + jwt_headers = github3.apps.create_jwt_headers(gh_app_private_key_bytes, gh_app_id) + url = f"https://api.github.com/app/installations/{gh_app_installation_id}/access_tokens" + + try: + response = requests.post(url, headers=jwt_headers, json=None, timeout=5) + response.raise_for_status() + except requests.exceptions.RequestException as e: + print(f"Request failed: {e}") + return None + return response.json().get("token") diff --git a/issue_metrics.py b/issue_metrics.py index 330d79bd..b9809642 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -11,7 +11,6 @@ search_issues(search_query: str, github_connection: github3.GitHub) -> github3.structs.SearchIterator: Searches for issues in a GitHub repository that match the given search query. - auth_to_github() -> github3.GitHub: Connect to GitHub API with token authentication. get_per_issue_metrics(issues: Union[List[dict], List[github3.issues.Issue]], discussions: bool = False), labels: Union[List[str], None] = None, ignore_users: List[str] = [] -> tuple[List, int, int]: @@ -25,6 +24,7 @@ from typing import List, Union import github3 +from auth import auth_to_github, get_github_app_installation_token from classes import IssueWithMetrics from config import EnvVars, get_env_vars from discussions import get_discussions @@ -92,38 +92,6 @@ def search_issues( return issues -def auth_to_github( - gh_app_id: str, - gh_app_installation_id: int, - gh_app_private_key_bytes: bytes, - token: str, - ghe: str, -) -> github3.GitHub: - """ - Connect to GitHub.com or GitHub Enterprise, depending on env variables. - - Returns: - github3.GitHub: A github api connection. - """ - - if gh_app_id and gh_app_private_key_bytes and gh_app_installation_id: - gh = github3.github.GitHub() - gh.login_as_app_installation( - gh_app_private_key_bytes, gh_app_id, gh_app_installation_id - ) - github_connection = gh - elif ghe and token: - github_connection = github3.github.GitHubEnterprise(ghe, token=token) - elif token: - github_connection = github3.login(token=token) - else: - raise ValueError( - "GH_TOKEN or the set of [GH_APP_ID, GH_APP_INSTALLATION_ID, GH_APP_PRIVATE_KEY] environment variables are not set" - ) - - return github_connection # type: ignore - - def get_per_issue_metrics( issues: Union[List[dict], List[github3.search.IssueSearchResult]], # type: ignore env_vars: EnvVars, @@ -175,9 +143,9 @@ def get_per_issue_metrics( issue_with_metrics.mentor_activity = count_comments_per_user( None, issue, - ignore_users, None, None, + ignore_users, max_comments_to_eval, heavily_involved, ) @@ -289,11 +257,20 @@ def main(): token = env_vars.gh_token ignore_users = env_vars.ignore_users + gh_app_id = env_vars.gh_app_id + gh_app_installation_id = env_vars.gh_app_installation_id + gh_app_private_key_bytes = env_vars.gh_app_private_key_bytes + + if not token and gh_app_id and gh_app_installation_id and gh_app_private_key_bytes: + token = get_github_app_installation_token( + gh_app_id, gh_app_private_key_bytes, gh_app_installation_id + ) + # Auth to GitHub.com github_connection = auth_to_github( - env_vars.gh_app_id, - env_vars.gh_app_installation_id, - env_vars.gh_app_private_key_bytes, + gh_app_id, + gh_app_installation_id, + gh_app_private_key_bytes, token, env_vars.ghe, ) diff --git a/most_active_mentors.py b/most_active_mentors.py index be0483e3..7a9a64e8 100755 --- a/most_active_mentors.py +++ b/most_active_mentors.py @@ -56,8 +56,8 @@ def count_comments_per_user( Args: issue (Union[github3.issues.Issue, None]): A GitHub issue. pull_request (Union[github3.pulls.PullRequest, None]): A GitHub pull - request. ignore_users (List[str]): A list of GitHub usernames to - ignore. + request. + ignore_users (List[str]): A list of GitHub usernames to ignore. max_comments_to_eval: Maximum number of comments per item to look at. heavily_involved: Maximum number of comments to count for one user per issue. diff --git a/test_auth.py b/test_auth.py new file mode 100644 index 00000000..7057ff70 --- /dev/null +++ b/test_auth.py @@ -0,0 +1,72 @@ +"""A module containing unit tests for the auth module. + +This module contains unit tests for the functions in the auth module +that authenticate to github. + +Classes: + TestAuthToGithub: A class to test the auth_to_github function. + +""" + +import unittest +from unittest.mock import MagicMock, patch + +import github3 +from auth import auth_to_github, get_github_app_installation_token + + +class TestAuthToGithub(unittest.TestCase): + """Test the auth_to_github function.""" + + @patch("github3.github.GitHub.login_as_app_installation") + def test_auth_to_github_with_github_app(self, mock_login): + """ + Test the auth_to_github function when GitHub app + parameters provided. + """ + mock_login.return_value = MagicMock() + result = auth_to_github(12345, 678910, b"hello", "", "") + + self.assertIsInstance(result, github3.github.GitHub) + + def test_auth_to_github_with_token(self): + """ + Test the auth_to_github function when the token is provided. + """ + result = auth_to_github(None, None, b"", "token", "") + + self.assertIsInstance(result, github3.github.GitHub) + + def test_auth_to_github_without_authentication_information(self): + """ + Test the auth_to_github function when authentication information is not provided. + Expect a ValueError to be raised. + """ + with self.assertRaises(ValueError): + auth_to_github(None, None, b"", "", "") + + def test_auth_to_github_with_ghe(self): + """ + Test the auth_to_github function when the GitHub Enterprise URL is provided. + """ + result = auth_to_github(None, None, b"", "token", "https://github.example.com") + + self.assertIsInstance(result, github3.github.GitHubEnterprise) + + @patch("github3.apps.create_jwt_headers", MagicMock(return_value="gh_token")) + @patch("requests.post") + def test_get_github_app_installation_token(self, mock_post): + """ + Test the get_github_app_installation_token function. + """ + dummy_token = "dummytoken" + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = {"token": dummy_token} + mock_post.return_value = mock_response + + result = get_github_app_installation_token( + b"gh_private_token", "gh_app_id", "gh_installation_id" + ) + + self.assertEqual(result, dummy_token) diff --git a/test_issue_metrics.py b/test_issue_metrics.py index 60a11003..476b5986 100644 --- a/test_issue_metrics.py +++ b/test_issue_metrics.py @@ -6,7 +6,6 @@ Classes: TestSearchIssues: A class to test the search_issues function. - TestAuthToGithub: A class to test the auth_to_github function. TestGetPerIssueMetrics: A class to test the get_per_issue_metrics function. TestGetEnvVars: A class to test the get_env_vars function. TestMain: A class to test the main function. @@ -18,11 +17,9 @@ from datetime import datetime, timedelta from unittest.mock import MagicMock, patch -import github3 import issue_metrics from issue_metrics import ( IssueWithMetrics, - auth_to_github, get_env_vars, get_owner_and_repository, get_per_issue_metrics, @@ -94,45 +91,6 @@ def test_get_owner_and_repository_without_either_in_query(self): self.assertIsNone(result.get("repository")) -class TestAuthToGithub(unittest.TestCase): - """Test the auth_to_github function.""" - - @patch("github3.github.GitHub.login_as_app_installation") - def test_auth_to_github_with_github_app(self, mock_login): - """ - Test the auth_to_github function when GitHub app - parameters provided. - """ - mock_login.return_value = MagicMock() - result = auth_to_github(12345, 678910, b"hello", "", "") - - self.assertIsInstance(result, github3.github.GitHub) - - def test_auth_to_github_with_token(self): - """ - Test the auth_to_github function when the token is provided. - """ - result = auth_to_github(None, None, b"", "token", "") - - self.assertIsInstance(result, github3.github.GitHub) - - def test_auth_to_github_without_authentication_information(self): - """ - Test the auth_to_github function when authentication information is not provided. - Expect a ValueError to be raised. - """ - with self.assertRaises(ValueError): - auth_to_github(None, None, b"", "", "") - - def test_auth_to_github_with_ghe(self): - """ - Test the auth_to_github function when the GitHub Enterprise URL is provided. - """ - result = auth_to_github(None, None, b"", "token", "https://github.example.com") - - self.assertIsInstance(result, github3.github.GitHubEnterprise) - - class TestGetEnvVars(unittest.TestCase): """Test suite for the get_env_vars function.""" diff --git a/test_most_active_mentors.py b/test_most_active_mentors.py index 9fc5db52..44664bdc 100755 --- a/test_most_active_mentors.py +++ b/test_most_active_mentors.py @@ -22,7 +22,7 @@ class TestCountCommentsPerUser(unittest.TestCase): """Test the count_comments_per_user function.""" - def test_count_comments_per_user(self): + def test_count_comments_per_user_limit(self): """Test that count_comments_per_user correctly counts user comments. This test mocks the GitHub connection and issue comments, and checks @@ -54,6 +54,45 @@ def test_count_comments_per_user(self): # Check the results self.assertEqual(result, expected_result) + def test_count_comments_per_user_with_ignores(self): + """Test that count_comments_per_user correctly counts user comments with some users ignored.""" + # Set up the mock GitHub issues + mock_issue1 = MagicMock() + mock_issue1.comments = 2 + mock_issue1.issue.user.login = "issue_owner" + mock_issue1.created_at = "2023-01-01T00:00:00Z" + + # Set up mock GitHub issue comments by several users + mock_issue1.issue.comments.return_value = [] + for i in range(5): + mock_comment1 = MagicMock() + mock_comment1.user.login = "very_active_user" + mock_comment1.created_at = datetime.fromisoformat( + f"2023-01-02T{i:02d}:00:00Z" + ) + # pylint: disable=maybe-no-member + mock_issue1.issue.comments.return_value.append(mock_comment1) + for i in range(5): + mock_comment1 = MagicMock() + mock_comment1.user.login = "very_active_user_ignored" + mock_comment1.created_at = datetime.fromisoformat( + f"2023-01-02T{i:02d}:00:00Z" + ) + # pylint: disable=maybe-no-member + mock_issue1.issue.comments.return_value.append(mock_comment1) + + # Call the function + result = count_comments_per_user( + mock_issue1, ignore_users=["very_active_user_ignored"] + ) + # Only the comments by "very_active_user" should be counted, + # so the count should be 3 since that is the threshold for heavily involved + expected_result = {"very_active_user": 3} + + # Check the results + self.assertEqual(result, expected_result) + self.assertNotIn("very_active_user_ignored", result) + def test_get_mentor_count(self): """Test that get_mentor_count correctly counts comments per user.""" mentor_activity = {"sue": 15, "bob": 10} From 3e0b7a069a1402b988fbb8c2373d77fa64d7d03c Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Mon, 29 Apr 2024 17:10:27 -0700 Subject: [PATCH 046/358] fix: handle multiple repos in search query Signed-off-by: Zack Koppert --- issue_metrics.py | 59 ++++++++------ test_issue_metrics.py | 173 ++++++++++-------------------------------- 2 files changed, 77 insertions(+), 155 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index b9809642..ef79e51c 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -8,7 +8,7 @@ Functions: get_env_vars() -> EnvVars: Get the environment variables for use in the script. - search_issues(search_query: str, github_connection: github3.GitHub) + search_issues(search_query: str, github_connection: github3.GitHub, owners_and_repositories: List[dict]) -> github3.structs.SearchIterator: Searches for issues in a GitHub repository that match the given search query. get_per_issue_metrics(issues: Union[List[dict], List[github3.issues.Issue]], @@ -43,7 +43,9 @@ def search_issues( - search_query: str, github_connection: github3.GitHub, owner: str, repository: str + search_query: str, + github_connection: github3.GitHub, + owners_and_repositories: List[dict], ) -> List[github3.search.IssueSearchResult]: # type: ignore """ Searches for issues/prs/discussions in a GitHub repository that match @@ -52,8 +54,8 @@ def search_issues( Args: search_query (str): The search query to use for finding issues/prs/discussions. github_connection (github3.GitHub): A connection to the GitHub API. - owner (str): The owner of the repository to search in. - repository (str): The repository to search in. + owners_and_repositories (List[dict]): A list of dictionaries containing + the owner and repository names. Returns: List[github3.search.IssueSearchResult]: A list of issues that match the search query. @@ -63,18 +65,22 @@ def search_issues( # Print the issue titles issues = [] + repos_and_owners_string = "" + for item in owners_and_repositories: + repos_and_owners_string += f"{item['owner']}/{item['repository']} " + try: for issue in issues_iterator: print(issue.title) # type: ignore issues.append(issue) except github3.exceptions.ForbiddenError: print( - f"You do not have permission to view this repository '{repository}'; Check your API Token." + f"You do not have permission to view a repository from: '{repos_and_owners_string}'; Check your API Token." ) sys.exit(1) except github3.exceptions.NotFoundError: print( - f"The repository could not be found; Check the repository owner and name: '{owner}/{repository}" + f"The repository could not be found; Check the repository owner and names: '{repos_and_owners_string}" ) sys.exit(1) except github3.exceptions.ConnectionError: @@ -212,28 +218,35 @@ def get_per_issue_metrics( return issues_with_metrics, num_issues_open, num_issues_closed -def get_owner_and_repository( +def get_owners_and_repositories( search_query: str, -) -> dict: - """Get the owner and repository from the search query. +) -> List[dict]: + """Get the owners and repositories from the search query. Args: search_query (str): The search query used to search for issues. Returns: - dict: A dictionary of owner and repository. + List[dict]: A list of dictionaries of owners and repositories. """ search_query_split = search_query.split(" ") - result = {} + results_list = [] for item in search_query_split: + result = {} if "repo:" in item and "/" in item: result["owner"] = item.split(":")[1].split("/")[0] result["repository"] = item.split(":")[1].split("/")[1] if "org:" in item or "owner:" in item or "user:" in item: result["owner"] = item.split(":")[1] + if "user:" in item: + result["owner"] = item.split(":")[1] + if "owner:" in item: + result["owner"] = item.split(":")[1] + if result: + results_list.append(result) - return result + return results_list def main(): @@ -279,17 +292,17 @@ def main(): max_comments_eval = int(env_vars.max_comments_eval) heavily_involved_cutoff = int(env_vars.heavily_involved_cutoff) - # Get the owner and repository from the search query - owner_and_repository = get_owner_and_repository(search_query) - owner = owner_and_repository.get("owner") - repository = owner_and_repository.get("repository") + # Get the owners and repositories from the search query + owners_and_repositories = get_owners_and_repositories(search_query) - if owner is None: - raise ValueError( - "The search query must include a repository owner and name \ - (ie. repo:owner/repo), an organization (ie. org:organization), \ - a user (ie. user:login) or an owner (ie. owner:user-or-organization)" - ) + # Every search query must include a repository owner for each repository, organization, or user + for item in owners_and_repositories: + if item["owner"] is None: + raise ValueError( + "The search query must include a repository owner and name \ + (ie. repo:owner/repo), an organization (ie. org:organization), \ + a user (ie. user:login) or an owner (ie. owner:user-or-organization)" + ) # Determine if there are label to measure labels = env_vars.labels_to_measure @@ -307,7 +320,7 @@ def main(): write_to_markdown(None, None, None, None, None, None, None, None) return else: - issues = search_issues(search_query, github_connection, owner, repository) + issues = search_issues(search_query, github_connection, owners_and_repositories) if len(issues) <= 0: print("No issues found") write_to_markdown(None, None, None, None, None, None, None, None) diff --git a/test_issue_metrics.py b/test_issue_metrics.py index 476b5986..1d41d3ba 100644 --- a/test_issue_metrics.py +++ b/test_issue_metrics.py @@ -17,11 +17,10 @@ from datetime import datetime, timedelta from unittest.mock import MagicMock, patch -import issue_metrics from issue_metrics import ( IssueWithMetrics, get_env_vars, - get_owner_and_repository, + get_owners_and_repositories, get_per_issue_metrics, measure_time_to_close, measure_time_to_first_response, @@ -52,43 +51,62 @@ def test_search_issues(self): mock_connection.search_issues.return_value = mock_issues # Call search_issues and check that it returns the correct issues - issues = search_issues( - "is:open", mock_connection, "fakeowner", "fakerepository" - ) + repo_with_owner = {"owner": "owner1", "repository": "repo1"} + owners_and_repositories = [repo_with_owner] + issues = search_issues("is:open", mock_connection, owners_and_repositories) self.assertEqual(issues, mock_issues) class TestGetOwnerAndRepository(unittest.TestCase): - """Unit tests for the get_owner_and_repository function. + """Unit tests for the get_owners_and_repositories function. - This class contains unit tests for the get_owner_and_repository function in the + This class contains unit tests for the get_owners_and_repositories function in the issue_metrics module. The tests use the unittest module and the unittest.mock module to mock the GitHub API and test the function in isolation. Methods: - test_get_owner_with_owner_and_repo_in_query: Test get both owner and repo. - test_get_owner_and_repository_with_repo_in_query: Test get just owner. - test_get_owner_and_repository_without_either_in_query: Test get neither. - + test_get_owners_with_owner_and_repo_in_query: Test get both owner and repo. + test_get_owners_and_repositories_with_repo_in_query: Test get just owner. + test_get_owners_and_repositories_without_either_in_query: Test get neither. + test_get_owners_and_repositories_with_multiple_entries: Test get multiple entries. """ - def test_get_owner_with_owner_and_repo_in_query(self): + def test_get_owners_with_owner_and_repo_in_query(self): """Test get both owner and repo.""" - result = get_owner_and_repository("repo:owner1/repo1") - self.assertEqual(result.get("owner"), "owner1") - self.assertEqual(result.get("repository"), "repo1") + result = get_owners_and_repositories("repo:owner1/repo1") + self.assertEqual(result[0].get("owner"), "owner1") + self.assertEqual(result[0].get("repository"), "repo1") - def test_get_owner_and_repository_with_repo_in_query(self): + def test_get_owner_and_repositories_with_repo_in_query(self): """Test get just owner.""" - result = get_owner_and_repository("org:owner1") - self.assertEqual(result.get("owner"), "owner1") - self.assertIsNone(result.get("repository")) + result = get_owners_and_repositories("org:owner1") + self.assertEqual(result[0].get("owner"), "owner1") + self.assertIsNone(result[0].get("repository")) - def test_get_owner_and_repository_without_either_in_query(self): + def test_get_owners_and_repositories_without_either_in_query(self): """Test get neither.""" - result = get_owner_and_repository("is:blah") - self.assertIsNone(result.get("owner")) - self.assertIsNone(result.get("repository")) + result = get_owners_and_repositories("is:blah") + self.assertEqual(result, []) + + def test_get_owners_and_repositories_with_multiple_entries(self): + """Test get multiple entries.""" + result = get_owners_and_repositories("repo:owner1/repo1 org:owner2") + self.assertEqual(result[0].get("owner"), "owner1") + self.assertEqual(result[0].get("repository"), "repo1") + self.assertEqual(result[1].get("owner"), "owner2") + self.assertIsNone(result[1].get("repository")) + + def test_get_owners_and_repositories_with_org(self): + """Test get org as owner.""" + result = get_owners_and_repositories("org:owner1") + self.assertEqual(result[0].get("owner"), "owner1") + self.assertIsNone(result[0].get("repository")) + + def test_get_owners_and_repositories_with_user(self): + """Test get user as owner.""" + result = get_owners_and_repositories("user:owner1") + self.assertEqual(result[0].get("owner"), "owner1") + self.assertIsNone(result[0].get("repository")) class TestGetEnvVars(unittest.TestCase): @@ -120,115 +138,6 @@ def test_get_env_vars_missing_query(self): get_env_vars(test=True) -class TestMain(unittest.TestCase): - """Unit tests for the main function. - - This class contains unit tests for the main function in the issue_metrics - module. The tests use the unittest module and the unittest.mock module to - mock the GitHub API and test the function in isolation. - - Methods: - test_main: Test that main runs without errors. - test_main_no_issues_found: Test that main handles when no issues are found - - """ - - @patch("issue_metrics.auth_to_github") - @patch("issue_metrics.search_issues") - @patch("issue_metrics.measure_time_to_first_response") - @patch("issue_metrics.get_stats_time_to_first_response") - @patch.dict( - os.environ, - { - "SEARCH_QUERY": "is:open repo:user/repo", - "GH_TOKEN": "test_token", - }, - ) - def test_main( - self, - mock_get_stats_time_to_first_response, - mock_measure_time_to_first_response, - mock_search_issues, - mock_auth_to_github, - ): - """Test that main runs without errors.""" - # Set up the mock GitHub connection object - mock_connection = MagicMock() - mock_auth_to_github.return_value = mock_connection - - # Set up the mock search_issues function - mock_issues = MagicMock( - items=[ - MagicMock(title="Issue 1"), - MagicMock(title="Issue 2"), - ] - ) - - mock_search_issues.return_value = mock_issues - - # Set up the mock measure_time_to_first_response function - mock_issues_with_ttfr = [ - ( - "Issue 1", - "https://github.com/user/repo/issues/1", - "alice", - timedelta(days=1, hours=2, minutes=30), - ), - ( - "Issue 2", - "https://github.com/user/repo/issues/2", - "bob", - timedelta(days=3, hours=4, minutes=30), - ), - ] - mock_measure_time_to_first_response.return_value = mock_issues_with_ttfr - - # Set up the mock get_stats_time_to_first_response function - mock_stats_time_to_first_response = 15 - mock_get_stats_time_to_first_response.return_value = ( - mock_stats_time_to_first_response - ) - - # Call main and check that it runs without errors - issue_metrics.main() - - # Remove the markdown file created by main - os.remove("issue_metrics.md") - - @patch("issue_metrics.auth_to_github") - @patch("issue_metrics.search_issues") - @patch("issue_metrics.write_to_markdown") - @patch.dict( - os.environ, - { - "SEARCH_QUERY": "is:open repo:org/repo", - "GH_TOKEN": "test_token", - }, - ) - def test_main_no_issues_found( - self, - mock_write_to_markdown, - mock_search_issues, - mock_auth_to_github, - ): - """Test that main writes 'No issues found' to the - console and calls write_to_markdown with None.""" - - # Set up the mock GitHub connection object - mock_connection = MagicMock() - mock_auth_to_github.return_value = mock_connection - - # Set up the mock search_issues function to return an empty list of issues - mock_issues = MagicMock(items=[]) - mock_search_issues.return_value = mock_issues - - # Call main and check that it writes 'No issues found' - issue_metrics.main() - mock_write_to_markdown.assert_called_once_with( - None, None, None, None, None, None, None, None - ) - - class TestGetPerIssueMetrics(unittest.TestCase): """Test suite for the get_per_issue_metrics function.""" From 68a633a2ede409acdb64ead20aec3a476993b78e Mon Sep 17 00:00:00 2001 From: jmeridth Date: Tue, 30 Apr 2024 01:15:44 -0500 Subject: [PATCH 047/358] feat: release on specific labels Related to https://github.com/github/github-ospo/issues/105 Only generate a release and new action container images if our semver related labels (`breaking`, `enhancement`, `fix`) or the `release` label are used on a merged pull request. Changed from push (merge) on main branch to release generation happening when a pull_request is merged to main branch. This gives us access to the pull requests labels without having to make API cals. Currently we'd still need to label a pull request with `release` if it is a dependabot or manual pull request related to a CVE or security fix. - [x] update CONTRIBUTING.md with new release information - [x] manually add `vuln` and `release` labels to repository - [x] add dependabot updates Signed-off-by: jmeridth --- .github/release-drafter.yml | 2 +- .github/workflows/release.yml | 14 +++++++++++++- CONTRIBUTING.md | 4 +++- requirements-test.txt | 4 ++-- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index a07d612d..33db4984 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -35,9 +35,9 @@ version-resolver: minor: labels: - 'enhancement' + - 'fix' patch: labels: - - 'fix' - 'documentation' - 'maintenance' default: patch diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1ae95f5c..687a4cb9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,9 @@ on: workflow_dispatch: - push: + pull_request: + types: + - closed branches: - main @@ -12,6 +14,16 @@ jobs: create_release: + # release if + # manual deployment OR + # merged to main and labelled with release labels + if: | + (github.event_name == 'workflow_dispatch') || + (github.event.pull_request.merged == true && + (contains(github.event.pull_request.labels.*.name, 'breaking') || + contains(github.event.pull_request.labels.*.name, 'enhancement') || + contains(github.event.pull_request.labels.*.name, 'vuln') || + contains(github.event.pull_request.labels.*.name, 'release'))) outputs: full-tag: ${{ steps.release-drafter.outputs.tag_name }} short-tag: ${{ steps.get_tag_name.outputs.SHORT_TAG }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e79d3e93..2db3bd6c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -77,4 +77,6 @@ We are using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0. ## Releases -Releases are automated but if you need to manually initiate a release you can do so through the GitHub Actions UI. If you have permissions to do so, you can navigate to the [Actions tab](https://github.com/github/issue-metrics/actions/workflows/release.yml) and select the `Run workflow` button. This will allow you to select the branch to release from and the version to release. +Releases are automated if a pull request is labelled with our [SemVer related labels](.github/release-drafter.yml) or with the `vuln` or `release` labels. + +You can also manually initiate a release you can do so through the GitHub Actions UI. If you have permissions to do so, you can navigate to the [Actions tab](https://github.com/github/issue-metrics/actions/workflows/release.yml) and select the `Run workflow` button. This will allow you to select the branch to release from and the version to release. diff --git a/requirements-test.txt b/requirements-test.txt index 641c7bd1..eebc014d 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,9 +1,9 @@ -black==24.4.1 +black==24.4.2 flake8==7.0.0 mypy==1.10.0 mypy-extensions==1.0.0 pylint==3.1.0 -pytest==8.1.1 +pytest==8.2.0 pytest-cov==5.0.0 types-pytz==2024.1.0.20240417 types-requests==2.31.0.20240406 From a26168d91cf82655ff6bd1b453165cb567c4fce6 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 30 Apr 2024 22:34:26 -0700 Subject: [PATCH 048/358] feat: Split markdown files when larger than max issue body size Signed-off-by: Zack Koppert --- .gitignore | 2 +- README.md | 1 + docs/dealing-with-large-issue-metrics.md | 16 +++++ issue_metrics.py | 15 +++++ markdown_helpers.py | 38 ++++++++++++ test_markdown_helpers.py | 75 ++++++++++++++++++++++++ 6 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 docs/dealing-with-large-issue-metrics.md create mode 100644 markdown_helpers.py create mode 100644 test_markdown_helpers.py diff --git a/.gitignore b/.gitignore index 5aab7254..4f454b1b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # Output files -issue_metrics.md +issue_metrics*.md issue_metrics.json # Byte-compiled / optimized / DLL files diff --git a/README.md b/README.md index 801ea852..c6b7ea96 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,7 @@ This action can be configured to authenticate with GitHub App Installation or Pe - [Configuring the `SEARCH_QUERY`](./docs/search-query.md) - [Local usage without Docker](./docs/local-usage-without-docker.md) - [Authenticating with GitHub App Installation](./docs/authenticating-with-github-app-installation.md) +- [Dealing with large issue_metrics.md files](./docs/dealing-with-large-issue-metrics.md) ## Contributions diff --git a/docs/dealing-with-large-issue-metrics.md b/docs/dealing-with-large-issue-metrics.md new file mode 100644 index 00000000..1a85e6c5 --- /dev/null +++ b/docs/dealing-with-large-issue-metrics.md @@ -0,0 +1,16 @@ +# Dealing with large issue metrics markdown files + +When working with lots of issues/pull requests/discussion results, the resulting issue_metrics.md file can become very large. This can cause the GitHub API to return an error when trying to create an issue with the contents of the file. + +```shell +Pull request creation failed. Validation failed: Body is too long (maximum is 65536 characters) +``` + +To work around this limitation, the issue-metrics action detects the issue and splits the issue_metrics.md file into smaller files. So instead of issue_metrics.md, you will get issue_metrics_0.md, issue_metrics_1.md, etc. Since we don't want the action to fail, it has been designed to have the same name as usual for the first split file (issue_metrics.md) and then append a number to the name for the subsequent split files. + +You can choose one of the following strategies to deal with the split files: +- Create multiple issues, each with using the next split file in the sequence. +- Upload the full file as an artifact and link to it in the issue body. +- Create an issue and put the content of the split files as issue comments. + +JSON output files are not split since its not anticipated that you use them as issue body content. diff --git a/issue_metrics.py b/issue_metrics.py index ef79e51c..ec852109 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -20,6 +20,7 @@ main(): Run the issue-metrics script. """ +import shutil import sys from typing import List, Union @@ -30,6 +31,7 @@ from discussions import get_discussions from json_writer import write_to_json from labels import get_label_metrics, get_stats_time_in_labels +from markdown_helpers import markdown_too_large_for_issue_body, split_markdown_file from markdown_writer import write_to_markdown from most_active_mentors import count_comments_per_user, get_mentor_count from time_to_answer import get_stats_time_to_answer, measure_time_to_answer @@ -364,6 +366,7 @@ def main(): num_mentor_count, search_query, ) + write_to_markdown( issues_with_metrics, stats_time_to_first_response, @@ -377,6 +380,18 @@ def main(): search_query, ) + max_char_count = 65535 + if markdown_too_large_for_issue_body("issue_metrics.md", max_char_count): + split_markdown_file("issue_metrics.md", max_char_count) + shutil.move("issue_metrics.md", "issue_metrics_full.md") + shutil.move("issue_metrics_0.md", "issue_metrics.md") + print( + "Issue metrics markdown file is too large for GitHub issue body and has been \ + split into multiple files. ie. issue_metrics.md, issue_metrics_1.md, etc. \ + The full file is saved as issue_metrics_full.md\n\ + See https://github.com/github/issue-metrics/blob/main/docs/dealing-with-large-issue-metrics.md" + ) + if __name__ == "__main__": main() diff --git a/markdown_helpers.py b/markdown_helpers.py new file mode 100644 index 00000000..8faf39ac --- /dev/null +++ b/markdown_helpers.py @@ -0,0 +1,38 @@ +""" Helper functions for working with markdown files. """ + + +def markdown_too_large_for_issue_body(file_path: str, max_char_count: int) -> bool: + """ + Check if the markdown file is too large to fit into a github issue. + + Inputs: + file_path: str - the path to the markdown file to check + max_char_count: int - the maximum number of characters allowed in a github issue body + + Returns: + bool - True if the file is too large, False otherwise + + """ + with open(file_path, "r", encoding="utf-8") as file: + file_contents = file.read() + return len(file_contents) > max_char_count + + +def split_markdown_file(file_path: str, max_char_count: int) -> None: + """ + Split the markdown file into smaller files. + + Inputs: + file_path: str - the path to the markdown file to split + max_char_count: int - the maximum number of characters allowed before splitting markdown file + + """ + with open(file_path, "r", encoding="utf-8") as file: + file_contents = file.read() + contents_list = [ + file_contents[i : i + max_char_count] + for i in range(0, len(file_contents), max_char_count) + ] + for i, content in enumerate(contents_list): + with open(f"{file_path[:-3]}_{i}.md", "w", encoding="utf-8") as new_file: + new_file.write(content) diff --git a/test_markdown_helpers.py b/test_markdown_helpers.py new file mode 100644 index 00000000..b09c739e --- /dev/null +++ b/test_markdown_helpers.py @@ -0,0 +1,75 @@ +""" Unit tests for the markdown_helpers module. """ + +import os +import unittest + +from markdown_helpers import markdown_too_large_for_issue_body, split_markdown_file + + +class TestMarkdownHelpers(unittest.TestCase): + """ + Unit tests for the markdown_helpers module. + """ + + def test_markdown_too_large_for_issue_body(self): + """ + Test the markdown_too_large_for_issue_body function. + """ + # Define a sample markdown file content + max_char_count = 65535 + markdown_content = "a\n" * max_char_count + + # Write the markdown content to a temporary file + with open("temp.md", "w", encoding="utf-8") as f: + f.write(markdown_content) + + # Call the function with the temporary file + result = markdown_too_large_for_issue_body("temp.md", max_char_count) + + # remove the temporary file + os.remove("temp.md") + + # Assert that the function returns True + self.assertTrue(result) + + def test_split_markdown_file(self): + """ + Test the split_markdown_file function. + """ + + # Define a sample markdown file content with 3 times the maximum character count + multiple_of_max = 4 + max_char_count = 65535 + repeated_content = "a\n" + markdown_content = repeated_content * int( + (max_char_count * multiple_of_max) / len(repeated_content) + ) + + # Write the markdown content to a temporary file + with open("temp.md", "w", encoding="utf-8") as f: + f.write(markdown_content) + + # Call the function with the temporary file + split_markdown_file("temp.md", max_char_count) + + # Assert that the function creates two files + self.assertTrue(os.path.exists("temp_0.md")) + self.assertTrue(os.path.exists("temp_1.md")) + self.assertTrue(os.path.exists("temp_2.md")) + self.assertTrue(os.path.exists("temp_3.md")) + + # Assert that the all files have less than max characters + for i in range(0, multiple_of_max): + with open(f"temp_{i}.md", "r", encoding="utf-8") as f: + self.assertLessEqual(len(f.read()), max_char_count) + + # remove the temporary files + os.remove("temp.md") + os.remove("temp_0.md") + os.remove("temp_1.md") + os.remove("temp_2.md") + os.remove("temp_3.md") + + +if __name__ == "__main__": + unittest.main() From c9c7a478d063a455511c5cbd313debd127c465d6 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 30 Apr 2024 22:46:08 -0700 Subject: [PATCH 049/358] fix: formatting errors Signed-off-by: Zack Koppert --- .github/linters/.flake8 | 2 +- docs/dealing-with-large-issue-metrics.md | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/linters/.flake8 b/.github/linters/.flake8 index 1aaf8292..de0917a5 100644 --- a/.github/linters/.flake8 +++ b/.github/linters/.flake8 @@ -1,5 +1,5 @@ [flake8] exclude = venv,.venv,.git,__pycache__ -extend-ignore = C901 +extend-ignore = C901, E203 max-line-length = 150 statistics = True diff --git a/docs/dealing-with-large-issue-metrics.md b/docs/dealing-with-large-issue-metrics.md index 1a85e6c5..d050b512 100644 --- a/docs/dealing-with-large-issue-metrics.md +++ b/docs/dealing-with-large-issue-metrics.md @@ -1,4 +1,4 @@ -# Dealing with large issue metrics markdown files +# Dealing with large issue metrics Markdown files When working with lots of issues/pull requests/discussion results, the resulting issue_metrics.md file can become very large. This can cause the GitHub API to return an error when trying to create an issue with the contents of the file. @@ -6,7 +6,8 @@ When working with lots of issues/pull requests/discussion results, the resulting Pull request creation failed. Validation failed: Body is too long (maximum is 65536 characters) ``` -To work around this limitation, the issue-metrics action detects the issue and splits the issue_metrics.md file into smaller files. So instead of issue_metrics.md, you will get issue_metrics_0.md, issue_metrics_1.md, etc. Since we don't want the action to fail, it has been designed to have the same name as usual for the first split file (issue_metrics.md) and then append a number to the name for the subsequent split files. +To work around this limitation, the issue-metrics action detects the issue and splits the issue_metrics.md file into smaller files. So instead of issue_metrics.md, you will get issue_metrics_0.md, issue_metrics_1.md, etc. +Since we don't want the action to fail, it has been designed to have the same name as usual for the first split file (issue_metrics.md) and then append a number to the name for the subsequent split files. You can choose one of the following strategies to deal with the split files: - Create multiple issues, each with using the next split file in the sequence. From 9e4b1f3f94f2826cc86c7c75f8150c57a457980f Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Wed, 1 May 2024 23:37:26 -0700 Subject: [PATCH 050/358] Update dealing-with-large-issue-metrics.md --- docs/dealing-with-large-issue-metrics.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dealing-with-large-issue-metrics.md b/docs/dealing-with-large-issue-metrics.md index d050b512..5c224516 100644 --- a/docs/dealing-with-large-issue-metrics.md +++ b/docs/dealing-with-large-issue-metrics.md @@ -1,6 +1,6 @@ # Dealing with large issue metrics Markdown files -When working with lots of issues/pull requests/discussion results, the resulting issue_metrics.md file can become very large. This can cause the GitHub API to return an error when trying to create an issue with the contents of the file. +When working with lots of issues/pull requests/discussion results, the resulting issue_metrics.md file can become very large. This can cause the GitHub API to return an error when trying to create an issue with the contents of the file. ```shell Pull request creation failed. Validation failed: Body is too long (maximum is 65536 characters) From 5a8575f0829d5ec719280f6c8ce57d122940e205 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Wed, 1 May 2024 23:37:53 -0700 Subject: [PATCH 051/358] more specific language --- docs/dealing-with-large-issue-metrics.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dealing-with-large-issue-metrics.md b/docs/dealing-with-large-issue-metrics.md index 5c224516..c2247f63 100644 --- a/docs/dealing-with-large-issue-metrics.md +++ b/docs/dealing-with-large-issue-metrics.md @@ -6,7 +6,7 @@ When working with lots of issues/pull requests/discussion results, the resulting Pull request creation failed. Validation failed: Body is too long (maximum is 65536 characters) ``` -To work around this limitation, the issue-metrics action detects the issue and splits the issue_metrics.md file into smaller files. So instead of issue_metrics.md, you will get issue_metrics_0.md, issue_metrics_1.md, etc. +To work around this limitation, the issue-metrics action detects the large file size and splits the issue_metrics.md file into smaller files. So instead of issue_metrics.md, you will get issue_metrics_0.md, issue_metrics_1.md, etc. Since we don't want the action to fail, it has been designed to have the same name as usual for the first split file (issue_metrics.md) and then append a number to the name for the subsequent split files. You can choose one of the following strategies to deal with the split files: From 744f9dc2ca547e2c8c75fea2747810064c26a3bd Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Wed, 1 May 2024 23:38:29 -0700 Subject: [PATCH 052/358] comment to match code --- test_markdown_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_markdown_helpers.py b/test_markdown_helpers.py index b09c739e..d856a62e 100644 --- a/test_markdown_helpers.py +++ b/test_markdown_helpers.py @@ -37,7 +37,7 @@ def test_split_markdown_file(self): Test the split_markdown_file function. """ - # Define a sample markdown file content with 3 times the maximum character count + # Define a sample markdown file content with 4 times the maximum character count multiple_of_max = 4 max_char_count = 65535 repeated_content = "a\n" From dd7d44eae74bdfdb46fdd6402a7bb9bba493d6fe Mon Sep 17 00:00:00 2001 From: Lawrence Wang Date: Thu, 2 May 2024 16:22:50 -0400 Subject: [PATCH 053/358] Add handling for API Rate Limiting --- issue_metrics.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index ec852109..77e02e07 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -23,6 +23,7 @@ import shutil import sys from typing import List, Union +from time import sleep import github3 from auth import auth_to_github, get_github_app_installation_token @@ -62,19 +63,38 @@ def search_issues( Returns: List[github3.search.IssueSearchResult]: A list of issues that match the search query. """ + + def wait_for_GH_api_refresh(iterator): + # Rate Limit Handling: API only allows 30 requests per minute + while iterator.ratelimit_remaining < 5: + print( + "Github API Rate Limit Low, waiting 1 minute to refresh" + ) + sleep(65) + + ISSUES_PER_PAGE = 100 + print("Searching for issues...") - issues_iterator = github_connection.search_issues(search_query, per_page=100) + issues_iterator = github_connection.search_issues( + search_query, per_page=ISSUES_PER_PAGE + ) + wait_for_GH_api_refresh(issues_iterator) + - # Print the issue titles issues = [] repos_and_owners_string = "" for item in owners_and_repositories: repos_and_owners_string += f"{item['owner']}/{item['repository']} " + # Print the issue titles try: - for issue in issues_iterator: + for idx, issue in enumerate(issues_iterator, 1): print(issue.title) # type: ignore issues.append(issue) + + # requests are sent once per page of issues + if idx % ISSUES_PER_PAGE == 0: + wait_for_GH_api_refresh(issues_iterator) except github3.exceptions.ForbiddenError: print( f"You do not have permission to view a repository from: '{repos_and_owners_string}'; Check your API Token." From bdfd787ec9013e5681ad1263f51fbc4ebffb7796 Mon Sep 17 00:00:00 2001 From: Lawrence Wang Date: Thu, 2 May 2024 16:52:19 -0400 Subject: [PATCH 054/358] update lint and test --- issue_metrics.py | 21 ++++++++++----------- test_issue_metrics.py | 11 +++++++++-- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index 77e02e07..e1ff563a 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -22,10 +22,11 @@ import shutil import sys -from typing import List, Union from time import sleep +from typing import List, Union import github3 +import github3.structs from auth import auth_to_github, get_github_app_installation_token from classes import IssueWithMetrics from config import EnvVars, get_env_vars @@ -64,22 +65,19 @@ def search_issues( List[github3.search.IssueSearchResult]: A list of issues that match the search query. """ - def wait_for_GH_api_refresh(iterator): + def wait_for_api_refresh(iterator: github3.structs.SearchIterator): # Rate Limit Handling: API only allows 30 requests per minute while iterator.ratelimit_remaining < 5: - print( - "Github API Rate Limit Low, waiting 1 minute to refresh" - ) + print("Github API Rate Limit Low, waiting 1 minute to refresh") sleep(65) - ISSUES_PER_PAGE = 100 + issues_per_page = 100 print("Searching for issues...") issues_iterator = github_connection.search_issues( - search_query, per_page=ISSUES_PER_PAGE + search_query, per_page=issues_per_page ) - wait_for_GH_api_refresh(issues_iterator) - + wait_for_api_refresh(issues_iterator) issues = [] repos_and_owners_string = "" @@ -93,8 +91,9 @@ def wait_for_GH_api_refresh(iterator): issues.append(issue) # requests are sent once per page of issues - if idx % ISSUES_PER_PAGE == 0: - wait_for_GH_api_refresh(issues_iterator) + if idx % issues_per_page == 0: + wait_for_api_refresh(issues_iterator) + except github3.exceptions.ForbiddenError: print( f"You do not have permission to view a repository from: '{repos_and_owners_string}'; Check your API Token." diff --git a/test_issue_metrics.py b/test_issue_metrics.py index 1d41d3ba..a0bb3e9f 100644 --- a/test_issue_metrics.py +++ b/test_issue_metrics.py @@ -42,13 +42,20 @@ class TestSearchIssues(unittest.TestCase): def test_search_issues(self): """Test that search_issues returns the correct issues.""" + # Set up the mock GitHub connection object - mock_connection = MagicMock() mock_issues = [ MagicMock(title="Issue 1"), MagicMock(title="Issue 2"), ] - mock_connection.search_issues.return_value = mock_issues + + # simulating github3.structs.SearchIterator return value + mock_search_result = MagicMock() + mock_search_result.__iter__.return_value = iter(mock_issues) + mock_search_result.ratelimit_remaining = 30 + + mock_connection = MagicMock() + mock_connection.search_issues.return_value = mock_search_result # Call search_issues and check that it returns the correct issues repo_with_owner = {"owner": "owner1", "repository": "repo1"} From f222ed7e041aef97b0a7a22b945e9fc546f0529d Mon Sep 17 00:00:00 2001 From: Lawrence Wang Date: Thu, 2 May 2024 17:09:35 -0400 Subject: [PATCH 055/358] Add exponential sleep increase --- issue_metrics.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index e1ff563a..be8327a1 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -65,11 +65,24 @@ def search_issues( List[github3.search.IssueSearchResult]: A list of issues that match the search query. """ + # Rate Limit Handling: API only allows 30 requests per minute def wait_for_api_refresh(iterator: github3.structs.SearchIterator): - # Rate Limit Handling: API only allows 30 requests per minute + max_retries = 5 + retry_count = 0 + sleep_time = 70 + while iterator.ratelimit_remaining < 5: - print("Github API Rate Limit Low, waiting 1 minute to refresh") - sleep(65) + if retry_count >= max_retries: + raise RuntimeError("Exceeded maximum retries for API rate limit") + + print( + f"GitHub API Rate Limit Low, waiting {sleep_time} seconds to refresh." + ) + sleep(sleep_time) + + # Exponentially increase the sleep time for the next retry + sleep_time *= 2 + retry_count += 1 issues_per_page = 100 From a8bf01dafc5ff30db9633085fe89b094599eb04e Mon Sep 17 00:00:00 2001 From: jmeridth Date: Tue, 7 May 2024 07:22:06 -0500 Subject: [PATCH 056/358] chore: add OSSF scorecard github action and README badge Relates to github/github-ospo#84 Relates to github/github-ospo#95 - [x] setup OSSF scorecard github action - [x] setup OSSF scorecard readme badge - [x] change current GitHub Actions to use SHAs instead of tags Signed-off-by: jmeridth --- .github/workflows/auto-labeler.yml | 2 +- .github/workflows/codeql-analysis.yml | 33 ++------------- .github/workflows/contributor_report.yaml | 4 +- .github/workflows/docker-image.yml | 2 +- .github/workflows/linter.yaml | 4 +- .github/workflows/major-version-updater.yml | 2 +- .github/workflows/pr-title.yml | 2 +- .github/workflows/python-package.yml | 4 +- .github/workflows/release.yml | 12 +++--- .github/workflows/scorecard.yml | 47 +++++++++++++++++++++ .github/workflows/stale.yaml | 2 +- README.md | 5 ++- 12 files changed, 72 insertions(+), 47 deletions(-) create mode 100644 .github/workflows/scorecard.yml diff --git a/.github/workflows/auto-labeler.yml b/.github/workflows/auto-labeler.yml index a13ee68c..8f9a5afd 100644 --- a/.github/workflows/auto-labeler.yml +++ b/.github/workflows/auto-labeler.yml @@ -21,7 +21,7 @@ name: Auto label pull requests runs-on: ubuntu-latest steps: - - uses: release-drafter/release-drafter@v6 + - uses: release-drafter/release-drafter@3f0f87098bd6b5c5b9a36d49c41d998ea58f9348 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 78ba9133..665e1d0c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -34,39 +34,14 @@ jobs: fail-fast: false matrix: language: [ 'python' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] - # Learn more: - # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed - steps: - name: Checkout repository - uses: actions/checkout@v4 - - # Initializes the CodeQL tools for scanning. + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@d39d31e687223d841ef683f52467bd88e9b21c14 with: languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - + uses: github/codeql-action/autobuild@d39d31e687223d841ef683f52467bd88e9b21c14 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@d39d31e687223d841ef683f52467bd88e9b21c14 diff --git a/.github/workflows/contributor_report.yaml b/.github/workflows/contributor_report.yaml index 80e796d7..c3f14dc9 100644 --- a/.github/workflows/contributor_report.yaml +++ b/.github/workflows/contributor_report.yaml @@ -28,7 +28,7 @@ jobs: echo "END_DATE=$end_date" >> "$GITHUB_ENV" - name: Run contributor action - uses: github/contributors@v1 + uses: github/contributors@832b6518181710ef277bc9ddafda6696e6b312bd env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} START_DATE: ${{ env.START_DATE }} @@ -37,7 +37,7 @@ jobs: SPONSOR_INFO: "true" - name: Create issue - uses: peter-evans/create-issue-from-file@v5 + uses: peter-evans/create-issue-from-file@24452a72d85239eacf1468b0f1982a9f3fec4c94 with: title: Monthly contributor report token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index ba0371a0..f4d9291f 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -14,6 +14,6 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b - name: Build the Docker image run: docker build . --file Dockerfile --platform linux/amd64 --tag issue-metrics:"$(date +%s)" diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index 280cb6a5..e5ba1621 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -18,7 +18,7 @@ jobs: statuses: write steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b with: # Full git history is needed to get a proper # list of changed files within `super-linter` @@ -28,7 +28,7 @@ jobs: python -m pip install --upgrade pip pip install -r requirements.txt -r requirements-test.txt - name: Lint Code Base - uses: super-linter/super-linter@v6 + uses: super-linter/super-linter@4758be622215d0954c8353ee4877ffd60111cf8e env: DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/major-version-updater.yml b/.github/workflows/major-version-updater.yml index 70af85df..58a85c5c 100644 --- a/.github/workflows/major-version-updater.yml +++ b/.github/workflows/major-version-updater.yml @@ -15,7 +15,7 @@ jobs: contents: write steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b - name: version id: version diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index de39d118..8331afde 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -20,7 +20,7 @@ jobs: name: Validate PR title runs-on: ubuntu-latest steps: - - uses: amannn/action-semantic-pull-request@v5 + - uses: amannn/action-semantic-pull-request@e9fabac35e210fea40ca5b14c0da95a099eff26f env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 3cd4d7fc..bdbdc874 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -22,9 +22,9 @@ jobs: python-version: [3.11, 3.12] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 687a4cb9..42cb9c20 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,7 +33,7 @@ contents: write pull-requests: read steps: - - uses: release-drafter/release-drafter@v6 + - uses: release-drafter/release-drafter@3f0f87098bd6b5c5b9a36d49c41d998ea58f9348 id: release-drafter env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -55,17 +55,17 @@ IMAGE_NAME: github/issue_metrics # different than repo name (underscore instead of dash) steps: - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb - name: Log in to the Container registry - uses: docker/login-action@v3 + uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/checkout@v4 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b - name: Push Docker Image if: ${{ success() }} - uses: docker/build-push-action@v5 + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 with: context: . file: ./Dockerfile @@ -84,7 +84,7 @@ discussions: write steps: - name: Create an announcement discussion for release - uses: abirismyname/create-discussion@v1.2.0 + uses: abirismyname/create-discussion@6e6ef67e5eeb042343ef8b3d8d0f5d545cbdf024 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 00000000..ac47aaf6 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,47 @@ +--- +name: Scorecard supply-chain security +on: + workflow_dispatch: + # For Branch-Protection check (for repo branch protection or rules). + # Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '29 11 * * 6' + push: + branches: ["main"] + +permissions: read-all + +jobs: + analysis: + name: Merge to Main Scorecard analysis + runs-on: ubuntu-latest + permissions: + security-events: write + id-token: write + + steps: + - name: "Checkout code" + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + - name: "Upload artifact" + uses: actions/upload-artifact@97a0fba1372883ab732affbe8f94b823f91727db # v3.pre.node20 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 + with: + sarif_file: results.sarif diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index bad2aeba..5a0bc415 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -11,7 +11,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v9 + - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e with: stale-issue-message: 'This issue is stale because it has been open 21 days with no activity. Remove stale label or comment or this will be closed in 14 days.' close-issue-message: 'This issue was closed because it has been stalled for 35 days with no activity.' diff --git a/README.md b/README.md index c6b7ea96..d2bec8a8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # Issue Metrics Action -[![CodeQL](https://github.com/github/issue-metrics/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/github/issue-metrics/actions/workflows/codeql-analysis.yml) [![Docker Image CI](https://github.com/github/issue-metrics/actions/workflows/docker-image.yml/badge.svg)](https://github.com/github/issue-metrics/actions/workflows/docker-image.yml) [![Python package](https://github.com/github/issue-metrics/actions/workflows/python-package.yml/badge.svg)](https://github.com/github/issue-metrics/actions/workflows/python-package.yml) +[![CodeQL](https://github.com/github/issue-metrics/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/github/issue-metrics/actions/workflows/codeql-analysis.yml) +[![Docker Image CI](https://github.com/github/issue-metrics/actions/workflows/docker-image.yml/badge.svg)](https://github.com/github/issue-metrics/actions/workflows/docker-image.yml) +[![Python package](https://github.com/github/issue-metrics/actions/workflows/python-package.yml/badge.svg)](https://github.com/github/issue-metrics/actions/workflows/python-package.yml) +[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/github/issue-metrics/badge)](https://scorecard.dev/viewer/?uri=github.com/github/issue-metrics) This is a GitHub Action that searches for issues/pull requests/discussions in a repository, measures several metrics, and generates a report in form of a GitHub issue. The issues/pull requests/discussions to search for can be filtered by using a search query. From fe6e72a7360d926d3ca55db291e3d9f919938991 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Tue, 7 May 2024 16:29:59 -0500 Subject: [PATCH 057/358] chore: group dependabot dependency updates for minor/patch updates Closes #268, Closes #269, Closes #270, Close #271, Close #272 To minimize the number of pull requests we get from dependabot, using groups will help with this. Still want major semver changes to be single PRs so that stand out and we pay particular attention to them. - [x] handle our multiple github action updates while in here. Signed-off-by: jmeridth --- .github/dependabot.yml | 18 ++++++++++++++++++ .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/contributor_report.yaml | 2 +- .github/workflows/docker-image.yml | 2 +- .github/workflows/linter.yaml | 2 +- .github/workflows/major-version-updater.yml | 2 +- .github/workflows/pr-title.yml | 2 +- .github/workflows/python-package.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/scorecard.yml | 6 +++--- 10 files changed, 29 insertions(+), 11 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 06abb029..9c47c599 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,15 +7,33 @@ updates: interval: "daily" commit-message: prefix: "chore(deps)" + groups: + dependencies: + applies-to: version-updates + update-types: + - "minor" + - "patch" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" commit-message: prefix: "chore(deps)" + groups: + dependencies: + applies-to: version-updates + update-types: + - "minor" + - "patch" - package-ecosystem: "docker" directory: "/" schedule: interval: "daily" commit-message: prefix: "chore(deps)" + groups: + dependencies: + applies-to: version-updates + update-types: + - "minor" + - "patch" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 665e1d0c..6a020cde 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -36,7 +36,7 @@ jobs: language: [ 'python' ] steps: - name: Checkout repository - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 - name: Initialize CodeQL uses: github/codeql-action/init@d39d31e687223d841ef683f52467bd88e9b21c14 with: diff --git a/.github/workflows/contributor_report.yaml b/.github/workflows/contributor_report.yaml index c3f14dc9..2c528f4b 100644 --- a/.github/workflows/contributor_report.yaml +++ b/.github/workflows/contributor_report.yaml @@ -28,7 +28,7 @@ jobs: echo "END_DATE=$end_date" >> "$GITHUB_ENV" - name: Run contributor action - uses: github/contributors@832b6518181710ef277bc9ddafda6696e6b312bd + uses: github/contributors@fa291c69abb946173a963a32f20ee29e8a7b6775 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} START_DATE: ${{ env.START_DATE }} diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index f4d9291f..55cae41d 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -14,6 +14,6 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b + - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 - name: Build the Docker image run: docker build . --file Dockerfile --platform linux/amd64 --tag issue-metrics:"$(date +%s)" diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index e5ba1621..08a3da6e 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -18,7 +18,7 @@ jobs: statuses: write steps: - name: Checkout Code - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: # Full git history is needed to get a proper # list of changed files within `super-linter` diff --git a/.github/workflows/major-version-updater.yml b/.github/workflows/major-version-updater.yml index 58a85c5c..6e5c012d 100644 --- a/.github/workflows/major-version-updater.yml +++ b/.github/workflows/major-version-updater.yml @@ -15,7 +15,7 @@ jobs: contents: write steps: - name: Checkout Repo - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 - name: version id: version diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index 8331afde..50a04a83 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -20,7 +20,7 @@ jobs: name: Validate PR title runs-on: ubuntu-latest steps: - - uses: amannn/action-semantic-pull-request@e9fabac35e210fea40ca5b14c0da95a099eff26f + - uses: amannn/action-semantic-pull-request@cfb60706e18bc85e8aec535e3c577abe8f70378e env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index bdbdc874..a194b091 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -22,7 +22,7 @@ jobs: python-version: [3.11, 3.12] steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b + - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 42cb9c20..32649d42 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,7 +62,7 @@ registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b + - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 - name: Push Docker Image if: ${{ success() }} uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index ac47aaf6..00d8ce65 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -25,7 +25,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: persist-credentials: false @@ -36,12 +36,12 @@ jobs: results_format: sarif publish_results: true - name: "Upload artifact" - uses: actions/upload-artifact@97a0fba1372883ab732affbe8f94b823f91727db # v3.pre.node20 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: SARIF file path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 + uses: github/codeql-action/upload-sarif@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.24.9 with: sarif_file: results.sarif From 2081ea5ac3b8999915acc6ad973512d62ad7cb31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 May 2024 17:13:25 +0000 Subject: [PATCH 058/358] chore(deps): bump the dependencies group with 2 updates Bumps the dependencies group with 2 updates: [github/codeql-action](https://github.com/github/codeql-action) and [github/contributors](https://github.com/github/contributors). Updates `github/codeql-action` from 3.25.3 to 3.25.4 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/d39d31e687223d841ef683f52467bd88e9b21c14...ccf74c947955fd1cf117aef6a0e4e66191ef6f61) Updates `github/contributors` from 1.4.2 to 1.4.3 - [Release notes](https://github.com/github/contributors/releases) - [Commits](https://github.com/github/contributors/compare/fa291c69abb946173a963a32f20ee29e8a7b6775...135b0430e856ade27175cbd1d4e1e11b0dd8ef95) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: github/contributors dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/contributor_report.yaml | 2 +- .github/workflows/scorecard.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 6a020cde..8c104cf8 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,10 +38,10 @@ jobs: - name: Checkout repository uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 - name: Initialize CodeQL - uses: github/codeql-action/init@d39d31e687223d841ef683f52467bd88e9b21c14 + uses: github/codeql-action/init@ccf74c947955fd1cf117aef6a0e4e66191ef6f61 with: languages: ${{ matrix.language }} - name: Autobuild - uses: github/codeql-action/autobuild@d39d31e687223d841ef683f52467bd88e9b21c14 + uses: github/codeql-action/autobuild@ccf74c947955fd1cf117aef6a0e4e66191ef6f61 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@d39d31e687223d841ef683f52467bd88e9b21c14 + uses: github/codeql-action/analyze@ccf74c947955fd1cf117aef6a0e4e66191ef6f61 diff --git a/.github/workflows/contributor_report.yaml b/.github/workflows/contributor_report.yaml index 2c528f4b..6ada8406 100644 --- a/.github/workflows/contributor_report.yaml +++ b/.github/workflows/contributor_report.yaml @@ -28,7 +28,7 @@ jobs: echo "END_DATE=$end_date" >> "$GITHUB_ENV" - name: Run contributor action - uses: github/contributors@fa291c69abb946173a963a32f20ee29e8a7b6775 + uses: github/contributors@135b0430e856ade27175cbd1d4e1e11b0dd8ef95 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} START_DATE: ${{ env.START_DATE }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 00d8ce65..92e61075 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.24.9 + uses: github/codeql-action/upload-sarif@ccf74c947955fd1cf117aef6a0e4e66191ef6f61 # v3.24.9 with: sarif_file: results.sarif From d7533d6d80b4e757c9604fc53bc81d7384a5e001 Mon Sep 17 00:00:00 2001 From: Jason Meridth Date: Wed, 8 May 2024 13:01:15 -0500 Subject: [PATCH 059/358] chore: delete unneeded custom codeql GitHub Action We do not have any custom CodeQL jobs, so we are going back to the default CodeQL setup. [New default CodeQL setup is already running successfully](https://github.com/github/issue-metrics/actions/workflows/github-code-scanning/codeql) --- .github/workflows/codeql-analysis.yml | 47 --------------------------- 1 file changed, 47 deletions(-) delete mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 8c104cf8..00000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,47 +0,0 @@ ---- -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ main ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ main ] - schedule: - - cron: '27 19 * * 5' - -permissions: - contents: read - security-events: write - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - language: [ 'python' ] - steps: - - name: Checkout repository - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 - - name: Initialize CodeQL - uses: github/codeql-action/init@ccf74c947955fd1cf117aef6a0e4e66191ef6f61 - with: - languages: ${{ matrix.language }} - - name: Autobuild - uses: github/codeql-action/autobuild@ccf74c947955fd1cf117aef6a0e4e66191ef6f61 - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@ccf74c947955fd1cf117aef6a0e4e66191ef6f61 From 94474f2833001f6f7afd93db42717c84a3304007 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 May 2024 17:51:30 +0000 Subject: [PATCH 060/358] chore(deps): bump ossf/scorecard-action in the dependencies group Bumps the dependencies group with 1 update: [ossf/scorecard-action](https://github.com/ossf/scorecard-action). Updates `ossf/scorecard-action` from 2.3.1 to 2.3.3 - [Release notes](https://github.com/ossf/scorecard-action/releases) - [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md) - [Commits](https://github.com/ossf/scorecard-action/compare/0864cf19026789058feabb7e87baa5f140aac736...dc50aa9510b46c811795eb24b2f1ba02a914e534) --- updated-dependencies: - dependency-name: ossf/scorecard-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 92e61075..2023613b 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 + uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.3 with: results_file: results.sarif results_format: sarif From 86db7b4e5db4aa8ce19bc5d313a825a3e6519f3c Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Fri, 10 May 2024 10:57:28 -0700 Subject: [PATCH 061/358] chore: Switch to weekly dep updates to reduce noise --- .github/dependabot.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9c47c599..c136da44 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,7 +4,7 @@ updates: - package-ecosystem: "pip" # See documentation for possible values directory: "/" # Location of package manifests schedule: - interval: "daily" + interval: "weekly" commit-message: prefix: "chore(deps)" groups: @@ -16,7 +16,7 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "daily" + interval: "weekly" commit-message: prefix: "chore(deps)" groups: @@ -28,7 +28,7 @@ updates: - package-ecosystem: "docker" directory: "/" schedule: - interval: "daily" + interval: "weekly" commit-message: prefix: "chore(deps)" groups: From df7fd8dc5785bd8e402ae54508dcb969fd69e224 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 17:15:27 +0000 Subject: [PATCH 062/358] chore(deps): bump github/codeql-action in the dependencies group Bumps the dependencies group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 3.25.4 to 3.25.5 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/ccf74c947955fd1cf117aef6a0e4e66191ef6f61...b7cec7526559c32f1616476ff32d17ba4c59b2d6) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 2023613b..9e7e7f83 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@ccf74c947955fd1cf117aef6a0e4e66191ef6f61 # v3.24.9 + uses: github/codeql-action/upload-sarif@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.24.9 with: sarif_file: results.sarif From 3d3bafe09d6d1f907d2cc8baa3c9ffa1e5495de7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 17:48:17 +0000 Subject: [PATCH 063/358] chore(deps): bump pylint from 3.1.0 to 3.1.1 in the dependencies group Bumps the dependencies group with 1 update: [pylint](https://github.com/pylint-dev/pylint). Updates `pylint` from 3.1.0 to 3.1.1 - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v3.1.0...v3.1.1) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index eebc014d..b7d4e5b8 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,7 +2,7 @@ black==24.4.2 flake8==7.0.0 mypy==1.10.0 mypy-extensions==1.0.0 -pylint==3.1.0 +pylint==3.1.1 pytest==8.2.0 pytest-cov==5.0.0 types-pytz==2024.1.0.20240417 From e553bf3594cd238b40780a513739b02540c247f0 Mon Sep 17 00:00:00 2001 From: Steven Stone Date: Thu, 16 May 2024 20:06:05 -0400 Subject: [PATCH 064/358] Address case where label was both labeled and unlabled --- labels.py | 2 +- test_labels.py | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/labels.py b/labels.py index 2bfcab38..9b4e4df0 100644 --- a/labels.py +++ b/labels.py @@ -74,7 +74,7 @@ def get_label_metrics(issue: github3.issues.Issue, labels: List[str]) -> dict: for label in labels: # if the label is still on there, add the time from the last event to now - if label in labeled and label not in unlabeled: + if label in labeled: # if the issue is closed, add the time from the issue creation to the closed_at time if issue.state == "closed": label_metrics[label] += datetime.fromisoformat( diff --git a/test_labels.py b/test_labels.py index 26f696f1..fecb2555 100644 --- a/test_labels.py +++ b/test_labels.py @@ -16,7 +16,7 @@ class TestLabels(unittest.TestCase): def setUp(self): self.issue = MagicMock() # type: ignore self.issue.issue = MagicMock(spec=github3.issues.Issue) # type: ignore - self.issue.created_at = "2020-01-01T00:00:00Z" + self.issue.created_at = "2021-01-01T00:00:00Z" self.issue.closed_at = "2021-01-05T00:00:00Z" self.issue.state = "closed" self.issue.issue.events.return_value = [ @@ -35,21 +35,28 @@ def setUp(self): label={"name": "bug"}, created_at=datetime(2021, 1, 3, tzinfo=pytz.UTC), ), + MagicMock( + event="labeled", + label={"name": "bug"}, + created_at=datetime(2021, 1, 4, tzinfo=pytz.UTC), + ), ] def test_get_label_events(self): """Test get_label_events""" labels = ["bug"] events = get_label_events(self.issue, labels) - self.assertEqual(len(events), 2) + self.assertEqual(len(events), 3) self.assertEqual(events[0].label["name"], "bug") self.assertEqual(events[1].label["name"], "bug") + self.assertEqual(events[2].label["name"], "bug") + def test_get_label_metrics_closed_issue(self): """Test get_label_metrics using a closed issue""" labels = ["bug", "feature"] metrics = get_label_metrics(self.issue, labels) - self.assertEqual(metrics["bug"], timedelta(days=2)) + self.assertEqual(metrics["bug"], timedelta(days=3)) self.assertEqual(metrics["feature"], timedelta(days=3)) def test_get_label_metrics_open_issue(self): @@ -57,7 +64,8 @@ def test_get_label_metrics_open_issue(self): self.issue.state = "open" labels = ["bug", "feature"] metrics = get_label_metrics(self.issue, labels) - self.assertEqual(metrics["bug"], timedelta(days=2)) + self.assertLessEqual(metrics["bug"], datetime.now(pytz.utc) - datetime(2021, 1, 2, tzinfo=pytz.UTC)) + self.assertGreater(metrics["bug"], datetime.now(pytz.utc) - datetime(2021, 1, 3, tzinfo=pytz.UTC)) self.assertLessEqual( metrics["feature"], datetime.now(pytz.utc) - datetime(2021, 1, 2, tzinfo=pytz.UTC), From 6623f77aaea818022d77f2a0af655203c139c759 Mon Sep 17 00:00:00 2001 From: Steven Stone Date: Thu, 16 May 2024 20:54:08 -0400 Subject: [PATCH 065/358] Run make lint --- test_labels.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test_labels.py b/test_labels.py index fecb2555..d5b6d434 100644 --- a/test_labels.py +++ b/test_labels.py @@ -51,7 +51,6 @@ def test_get_label_events(self): self.assertEqual(events[1].label["name"], "bug") self.assertEqual(events[2].label["name"], "bug") - def test_get_label_metrics_closed_issue(self): """Test get_label_metrics using a closed issue""" labels = ["bug", "feature"] @@ -64,8 +63,14 @@ def test_get_label_metrics_open_issue(self): self.issue.state = "open" labels = ["bug", "feature"] metrics = get_label_metrics(self.issue, labels) - self.assertLessEqual(metrics["bug"], datetime.now(pytz.utc) - datetime(2021, 1, 2, tzinfo=pytz.UTC)) - self.assertGreater(metrics["bug"], datetime.now(pytz.utc) - datetime(2021, 1, 3, tzinfo=pytz.UTC)) + self.assertLessEqual( + metrics["bug"], + datetime.now(pytz.utc) - datetime(2021, 1, 2, tzinfo=pytz.UTC), + ) + self.assertGreater( + metrics["bug"], + datetime.now(pytz.utc) - datetime(2021, 1, 3, tzinfo=pytz.UTC), + ) self.assertLessEqual( metrics["feature"], datetime.now(pytz.utc) - datetime(2021, 1, 2, tzinfo=pytz.UTC), From 6fd89f9be711068149018a5ba9f198c45435e679 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Sun, 19 May 2024 09:33:32 -0500 Subject: [PATCH 066/358] fix: allow access to forked pull request labels Release fails if pull request is from a fork currently because we can't access the labels on the forked pull request which helps us determine semver for next release. Saw this after merging https://github.com/github/issue-metrics/pull/283 (from a fork) Add pull_request_target to release action workflow I am not worried about the security implications with us checking out the forked pull requests code. This action only fires after a merge to main so this means the pull request code has been reviewed by a maintainer. We are post-CI/code run. Signed-off-by: jmeridth --- .github/workflows/release.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 32649d42..4b854126 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,6 +8,11 @@ - closed branches: - main + pull_request_target: + types: + - closed + branches: + - main permissions: contents: read From 16c00d0f8d02a105165b8d25f76eb9be882d67bb Mon Sep 17 00:00:00 2001 From: Steven Stone Date: Mon, 20 May 2024 10:57:18 -0400 Subject: [PATCH 067/358] Skip labeling events that have occured past issue close time --- labels.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/labels.py b/labels.py index 9b4e4df0..54198277 100644 --- a/labels.py +++ b/labels.py @@ -55,6 +55,10 @@ def get_label_metrics(issue: github3.issues.Issue, labels: List[str]) -> dict: # Calculate the time to add or subtract to the time spent in label based on the label events for event in label_events: + # Skip labeling events that have occured past issue close time + if issue.closed_at is not None and (event.created_at >= datetime.fromisoformat(issue.closed_at)): + continue + if event.event == "labeled": labeled[event.label["name"]] = True if event.label["name"] in labels: From 2d023ab9ea042ede74fb2338642794dc70578afb Mon Sep 17 00:00:00 2001 From: Steven Stone Date: Mon, 20 May 2024 12:18:51 -0400 Subject: [PATCH 068/358] Skip processing label time if last event is unlabled and issue is open --- labels.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/labels.py b/labels.py index 54198277..adbb3339 100644 --- a/labels.py +++ b/labels.py @@ -43,6 +43,7 @@ def get_label_metrics(issue: github3.issues.Issue, labels: List[str]) -> dict: """ label_metrics: dict = {} label_events = get_label_events(issue, labels) + label_last_event_type: dict = {} for label in labels: label_metrics[label] = None @@ -56,7 +57,9 @@ def get_label_metrics(issue: github3.issues.Issue, labels: List[str]) -> dict: # Calculate the time to add or subtract to the time spent in label based on the label events for event in label_events: # Skip labeling events that have occured past issue close time - if issue.closed_at is not None and (event.created_at >= datetime.fromisoformat(issue.closed_at)): + if issue.closed_at is not None and ( + event.created_at >= datetime.fromisoformat(issue.closed_at) + ): continue if event.event == "labeled": @@ -67,6 +70,7 @@ def get_label_metrics(issue: github3.issues.Issue, labels: List[str]) -> dict: label_metrics[ event.label["name"] ] -= event.created_at - datetime.fromisoformat(issue.created_at) + label_last_event_type[event.label["name"]] = "labeled" elif event.event == "unlabeled": unlabeled[event.label["name"]] = True if event.label["name"] in labels: @@ -75,9 +79,9 @@ def get_label_metrics(issue: github3.issues.Issue, labels: List[str]) -> dict: label_metrics[ event.label["name"] ] += event.created_at - datetime.fromisoformat(issue.created_at) + label_last_event_type[event.label["name"]] = "unlabeled" for label in labels: - # if the label is still on there, add the time from the last event to now if label in labeled: # if the issue is closed, add the time from the issue creation to the closed_at time if issue.state == "closed": @@ -85,6 +89,10 @@ def get_label_metrics(issue: github3.issues.Issue, labels: List[str]) -> dict: issue.closed_at ) - datetime.fromisoformat(issue.created_at) else: + # skip label if last labeling event is 'unlabled' and issue is still open + if label_last_event_type[label] == "unlabeled": + continue + # if the issue is open, add the time from the issue creation to now label_metrics[label] += datetime.now(pytz.utc) - datetime.fromisoformat( issue.created_at From 8815d92578b2983dfb90bbb6f875df800fa75316 Mon Sep 17 00:00:00 2001 From: Steven Stone Date: Mon, 20 May 2024 13:06:16 -0400 Subject: [PATCH 069/358] Add test for ensuring None time for label labeled past close date --- test_labels.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test_labels.py b/test_labels.py index d5b6d434..e7e8b5f7 100644 --- a/test_labels.py +++ b/test_labels.py @@ -40,6 +40,12 @@ def setUp(self): label={"name": "bug"}, created_at=datetime(2021, 1, 4, tzinfo=pytz.UTC), ), + # Label labeled after issue close date + MagicMock( + event="labeled", + label={"name": "foo"}, + created_at=datetime(2021, 1, 20, tzinfo=pytz.UTC), + ), ] def test_get_label_events(self): @@ -80,6 +86,13 @@ def test_get_label_metrics_open_issue(self): datetime.now(pytz.utc) - datetime(2021, 1, 4, tzinfo=pytz.UTC), ) + def test_get_label_metrics_closed_issue_labeled_past_closed_at(self): + """Test get_label_metrics using a closed issue that was labeled past issue closed_at""" + self.issue.state = "closed" + labels = ["foo"] + metrics = get_label_metrics(self.issue, labels) + self.assertEqual(metrics["foo"], None) + class TestGetAverageTimeInLabels(unittest.TestCase): """Unit tests for get_stats_time_in_labels""" From d73f102e860ea702dbebfe3503c8d8da8d03eb3c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 May 2024 17:19:39 +0000 Subject: [PATCH 070/358] chore(deps): bump the dependencies group with 3 updates Bumps the dependencies group with 3 updates: [actions/checkout](https://github.com/actions/checkout), [super-linter/super-linter](https://github.com/super-linter/super-linter) and [github/codeql-action](https://github.com/github/codeql-action). Updates `actions/checkout` from 4.1.5 to 4.1.6 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/44c2b7a8a4ea60a981eaca3cf939b5f4305c123b...a5ac7e51b41094c92402da3b24376905380afc29) Updates `super-linter/super-linter` from 6.4.1 to 6.5.0 - [Release notes](https://github.com/super-linter/super-linter/releases) - [Changelog](https://github.com/super-linter/super-linter/blob/main/CHANGELOG.md) - [Commits](https://github.com/super-linter/super-linter/compare/4758be622215d0954c8353ee4877ffd60111cf8e...56576d491db07c7236b445ab09991ca49d12b0c6) Updates `github/codeql-action` from 3.25.5 to 3.25.6 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/b7cec7526559c32f1616476ff32d17ba4c59b2d6...9fdb3e49720b44c48891d036bb502feb25684276) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: super-linter/super-linter dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/docker-image.yml | 2 +- .github/workflows/linter.yaml | 4 ++-- .github/workflows/major-version-updater.yml | 2 +- .github/workflows/python-package.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/scorecard.yml | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 55cae41d..f81f4477 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -14,6 +14,6 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Build the Docker image run: docker build . --file Dockerfile --platform linux/amd64 --tag issue-metrics:"$(date +%s)" diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index 08a3da6e..035f4935 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -18,7 +18,7 @@ jobs: statuses: write steps: - name: Checkout Code - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 with: # Full git history is needed to get a proper # list of changed files within `super-linter` @@ -28,7 +28,7 @@ jobs: python -m pip install --upgrade pip pip install -r requirements.txt -r requirements-test.txt - name: Lint Code Base - uses: super-linter/super-linter@4758be622215d0954c8353ee4877ffd60111cf8e + uses: super-linter/super-linter@56576d491db07c7236b445ab09991ca49d12b0c6 env: DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/major-version-updater.yml b/.github/workflows/major-version-updater.yml index 6e5c012d..330e4548 100644 --- a/.github/workflows/major-version-updater.yml +++ b/.github/workflows/major-version-updater.yml @@ -15,7 +15,7 @@ jobs: contents: write steps: - name: Checkout Repo - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: version id: version diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index a194b091..229d93a8 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -22,7 +22,7 @@ jobs: python-version: [3.11, 3.12] steps: - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 32649d42..86eb3ded 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,7 +62,7 @@ registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Push Docker Image if: ${{ success() }} uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 9e7e7f83..530c75f7 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -25,7 +25,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 with: persist-credentials: false @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.24.9 + uses: github/codeql-action/upload-sarif@9fdb3e49720b44c48891d036bb502feb25684276 # v3.24.9 with: sarif_file: results.sarif From c2c1cace182b4c59f63439952cba23ea8a5c698e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 May 2024 17:54:27 +0000 Subject: [PATCH 071/358] chore(deps): bump the dependencies group with 3 updates Bumps the dependencies group with 3 updates: [pylint](https://github.com/pylint-dev/pylint), [pytest](https://github.com/pytest-dev/pytest) and [requests](https://github.com/psf/requests). Updates `pylint` from 3.1.1 to 3.2.2 - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v3.1.1...v3.2.2) Updates `pytest` from 8.2.0 to 8.2.1 - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.2.0...8.2.1) Updates `requests` from 2.31.0 to 2.32.0 - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.31.0...v2.32.0) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: requests dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 4 ++-- requirements.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index b7d4e5b8..6d8fd6d7 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,8 +2,8 @@ black==24.4.2 flake8==7.0.0 mypy==1.10.0 mypy-extensions==1.0.0 -pylint==3.1.1 -pytest==8.2.0 +pylint==3.2.2 +pytest==8.2.1 pytest-cov==5.0.0 types-pytz==2024.1.0.20240417 types-requests==2.31.0.20240406 diff --git a/requirements.txt b/requirements.txt index c1d2d5fc..8506694d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ github3.py==4.0.1 numpy==1.26.4 python-dotenv==1.0.1 pytz==2024.1 -requests==2.31.0 +requests==2.32.0 From 901b160435106e031d6ab885ed846b1c47257fbe Mon Sep 17 00:00:00 2001 From: jmeridth Date: Thu, 23 May 2024 15:59:19 -0500 Subject: [PATCH 072/358] fix: issue accessing repo when not provided in query Fixes #290 - [x] handle trying to access repository from dictionary when not present (use default of empty string) - [x] add test to ensure we cover this scenario - [x] handle issue where `time_to_close` variable was being access before possibly being declared Signed-off-by: jmeridth --- issue_metrics.py | 4 +++- test_issue_metrics.py | 33 +++++++++++++++++++++++++++++---- time_to_close.py | 1 + 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index be8327a1..e5024a42 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -95,7 +95,9 @@ def wait_for_api_refresh(iterator: github3.structs.SearchIterator): issues = [] repos_and_owners_string = "" for item in owners_and_repositories: - repos_and_owners_string += f"{item['owner']}/{item['repository']} " + repos_and_owners_string += ( + f"{item.get('owner', '')}/{item.get('repository', '')} " + ) # Print the issue titles try: diff --git a/test_issue_metrics.py b/test_issue_metrics.py index a0bb3e9f..1cf6d1d9 100644 --- a/test_issue_metrics.py +++ b/test_issue_metrics.py @@ -36,12 +36,13 @@ class TestSearchIssues(unittest.TestCase): module to mock the GitHub API and test the function in isolation. Methods: - test_search_issues: Test that search_issues returns the correct issues. + test_search_issues_with_owner_and_repository: Test that search_issues with owner/repo returns the correct issues. + test_search_issues_with_just_owner_or_org: Test that search_issues with just an owner/org returns the correct issues. """ - def test_search_issues(self): - """Test that search_issues returns the correct issues.""" + def test_search_issues_with_owner_and_repository(self): + """Test that search_issues with owner/repo returns the correct issues.""" # Set up the mock GitHub connection object mock_issues = [ @@ -63,6 +64,30 @@ def test_search_issues(self): issues = search_issues("is:open", mock_connection, owners_and_repositories) self.assertEqual(issues, mock_issues) + def test_search_issues_with_just_owner_or_org(self): + """Test that search_issues with just an owner/org returns the correct issues.""" + + # Set up the mock GitHub connection object + mock_issues = [ + MagicMock(title="Issue 1"), + MagicMock(title="Issue 2"), + MagicMock(title="Issue 3"), + ] + + # simulating github3.structs.SearchIterator return value + mock_search_result = MagicMock() + mock_search_result.__iter__.return_value = iter(mock_issues) + mock_search_result.ratelimit_remaining = 30 + + mock_connection = MagicMock() + mock_connection.search_issues.return_value = mock_search_result + + # Call search_issues and check that it returns the correct issues + org = {"owner": "org1"} + owners = [org] + issues = search_issues("is:open", mock_connection, owners) + self.assertEqual(issues, mock_issues) + class TestGetOwnerAndRepository(unittest.TestCase): """Unit tests for the get_owners_and_repositories function. @@ -84,7 +109,7 @@ def test_get_owners_with_owner_and_repo_in_query(self): self.assertEqual(result[0].get("owner"), "owner1") self.assertEqual(result[0].get("repository"), "repo1") - def test_get_owner_and_repositories_with_repo_in_query(self): + def test_get_owner_and_repositories_without_repo_in_query(self): """Test get just owner.""" result = get_owners_and_repositories("org:owner1") self.assertEqual(result[0].get("owner"), "owner1") diff --git a/time_to_close.py b/time_to_close.py index a32262d1..7efecb24 100644 --- a/time_to_close.py +++ b/time_to_close.py @@ -75,6 +75,7 @@ def get_stats_time_to_close( # Calculate the total time to close for all issues close_times = [] + total_time_to_close = None if issues_with_time_to_close: total_time_to_close = 0 for issue in issues_with_time_to_close: From c0aee67786e1aac6c244358f5e6fae43386da53e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 17:44:17 +0000 Subject: [PATCH 073/358] chore(deps): bump super-linter/super-linter in the dependencies group Bumps the dependencies group with 1 update: [super-linter/super-linter](https://github.com/super-linter/super-linter). Updates `super-linter/super-linter` from 6.5.0 to 6.5.1 - [Release notes](https://github.com/super-linter/super-linter/releases) - [Changelog](https://github.com/super-linter/super-linter/blob/main/CHANGELOG.md) - [Commits](https://github.com/super-linter/super-linter/compare/56576d491db07c7236b445ab09991ca49d12b0c6...5b638caee6ba65e25e07143887b669a1233847a0) --- updated-dependencies: - dependency-name: super-linter/super-linter dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/linter.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index 035f4935..1a8a28ab 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -28,7 +28,7 @@ jobs: python -m pip install --upgrade pip pip install -r requirements.txt -r requirements-test.txt - name: Lint Code Base - uses: super-linter/super-linter@56576d491db07c7236b445ab09991ca49d12b0c6 + uses: super-linter/super-linter@5b638caee6ba65e25e07143887b669a1233847a0 env: DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 5821badc0f246f98050848c33efe5f1ef8503a67 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 18:01:38 +0000 Subject: [PATCH 074/358] chore(deps): bump the dependencies group with 2 updates Bumps the dependencies group with 2 updates: [types-requests](https://github.com/python/typeshed) and [requests](https://github.com/psf/requests). Updates `types-requests` from 2.31.0.20240406 to 2.32.0.20240523 - [Commits](https://github.com/python/typeshed/commits) Updates `requests` from 2.32.0 to 2.32.2 - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.32.0...v2.32.2) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: requests dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index 6d8fd6d7..bccb38da 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -6,4 +6,4 @@ pylint==3.2.2 pytest==8.2.1 pytest-cov==5.0.0 types-pytz==2024.1.0.20240417 -types-requests==2.31.0.20240406 +types-requests==2.32.0.20240523 diff --git a/requirements.txt b/requirements.txt index 8506694d..71dd5813 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ github3.py==4.0.1 numpy==1.26.4 python-dotenv==1.0.1 pytz==2024.1 -requests==2.32.0 +requests==2.32.2 From ee0c01d71f6a1da2936a6385e76a1326c958a2d6 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 28 May 2024 09:39:35 -0700 Subject: [PATCH 075/358] ci: release when a new feature hits main --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 84ada31b..9dd054b0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,7 @@ (github.event_name == 'workflow_dispatch') || (github.event.pull_request.merged == true && (contains(github.event.pull_request.labels.*.name, 'breaking') || - contains(github.event.pull_request.labels.*.name, 'enhancement') || + contains(github.event.pull_request.labels.*.name, 'feature') || contains(github.event.pull_request.labels.*.name, 'vuln') || contains(github.event.pull_request.labels.*.name, 'release'))) outputs: From a5b43e42ec4670bd810594f2b1ac032b0251821f Mon Sep 17 00:00:00 2001 From: jmeridth Date: Tue, 28 May 2024 12:13:56 -0500 Subject: [PATCH 076/358] chore: remove pull_request target if we already have pull_request_target - [x] auto-labeler action - [x] release action - [x] fix codeql bade in README (after we removed custom workflow in https://github.com/github/issue-metrics/pull/276) Signed-off-by: jmeridth --- .github/workflows/auto-labeler.yml | 6 +----- .github/workflows/release.yml | 5 ----- README.md | 2 +- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/.github/workflows/auto-labeler.yml b/.github/workflows/auto-labeler.yml index 8f9a5afd..9cb7f261 100644 --- a/.github/workflows/auto-labeler.yml +++ b/.github/workflows/auto-labeler.yml @@ -2,11 +2,7 @@ name: Auto Labeler on: - # pull_request event is required only for autolabeler - pull_request: - # Only following types are handled by the action, but one can default to all as well - types: [opened, reopened, synchronize] - # pull_request_target event is required for autolabeler to support PRs from forks + # pull_request_target event is required for autolabeler to support all PRs including forks pull_request_target: types: [opened, reopened, synchronize] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9dd054b0..70f5a32f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,11 +3,6 @@ on: workflow_dispatch: - pull_request: - types: - - closed - branches: - - main pull_request_target: types: - closed diff --git a/README.md b/README.md index d2bec8a8..456a781d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Issue Metrics Action -[![CodeQL](https://github.com/github/issue-metrics/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/github/issue-metrics/actions/workflows/codeql-analysis.yml) +[![CodeQL](https://github.com/github/issue-metrics/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/github/issue-metrics/actions/workflows/github-code-scanning/codeql) [![Docker Image CI](https://github.com/github/issue-metrics/actions/workflows/docker-image.yml/badge.svg)](https://github.com/github/issue-metrics/actions/workflows/docker-image.yml) [![Python package](https://github.com/github/issue-metrics/actions/workflows/python-package.yml/badge.svg)](https://github.com/github/issue-metrics/actions/workflows/python-package.yml) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/github/issue-metrics/badge)](https://scorecard.dev/viewer/?uri=github.com/github/issue-metrics) From 8d759bdc4fb4d221b19d9de7a23ca4af9d5b862e Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Wed, 29 May 2024 10:40:49 -0700 Subject: [PATCH 077/358] Add Jeff to review new data releases --- .github/pull_request_template.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index fed940d3..57f06b32 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -18,6 +18,7 @@ examples: "feat: add new logger" or "fix: remove unused imports" - [ ] If documentation is needed for this change, has that been included in this pull request - [ ] run `make lint` and fix any issues that you have introduced - [ ] run `make test` and ensure you have test coverage for the lines you are introducing +- [ ] If publishing new data to the public (scorecards, security scan results, code quality results, live dashboards, etc.), please request review from `@jeffrey-luszcz` ### Reviewer From ad132e5990054b5b813bec0de90c0b9f591a7d1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Jun 2024 17:57:25 +0000 Subject: [PATCH 078/358] chore(deps): bump the dependencies group with 2 updates Bumps the dependencies group with 2 updates: [docker/login-action](https://github.com/docker/login-action) and [github/codeql-action](https://github.com/github/codeql-action). Updates `docker/login-action` from 3.1.0 to 3.2.0 - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/e92390c5fb421da1463c202d546fed0ec5c39f20...0d4c9c5ea7693da7b068278f7b52bda2a190a446) Updates `github/codeql-action` from 3.25.6 to 3.25.7 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/9fdb3e49720b44c48891d036bb502feb25684276...f079b8493333aace61c81488f8bd40919487bd9f) --- updated-dependencies: - dependency-name: docker/login-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- .github/workflows/scorecard.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 70f5a32f..b1953deb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,7 +57,7 @@ - name: Set up Docker Buildx uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb - name: Log in to the Container registry - uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 + uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 530c75f7..e3784860 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@9fdb3e49720b44c48891d036bb502feb25684276 # v3.24.9 + uses: github/codeql-action/upload-sarif@f079b8493333aace61c81488f8bd40919487bd9f # v3.24.9 with: sarif_file: results.sarif From 21b01ccec150c69fefb476ee5726159f1fa9c4a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Jun 2024 18:05:53 +0000 Subject: [PATCH 079/358] chore(deps): bump requests in the dependencies group Bumps the dependencies group with 1 update: [requests](https://github.com/psf/requests). Updates `requests` from 2.32.2 to 2.32.3 - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.32.2...v2.32.3) --- updated-dependencies: - dependency-name: requests dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 71dd5813..a6f68343 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ github3.py==4.0.1 numpy==1.26.4 python-dotenv==1.0.1 pytz==2024.1 -requests==2.32.2 +requests==2.32.3 From 2b55fa8b46a1548eb037e39c64f6a02c2e8c42b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Jun 2024 18:13:59 +0000 Subject: [PATCH 080/358] chore(deps): bump types-requests from 2.32.0.20240523 to 2.32.0.20240602 Bumps [types-requests](https://github.com/python/typeshed) from 2.32.0.20240523 to 2.32.0.20240602. - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index bccb38da..cc2ca8ca 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -6,4 +6,4 @@ pylint==3.2.2 pytest==8.2.1 pytest-cov==5.0.0 types-pytz==2024.1.0.20240417 -types-requests==2.32.0.20240523 +types-requests==2.32.0.20240602 From 88a0fd04c9fe8f27a90839dce3d0eb6e03838275 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Thu, 6 Jun 2024 17:15:32 -0700 Subject: [PATCH 081/358] chore: add Jacob to pr reviews automatically --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0e5b7f49..cf1f49b2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @jmeridth @zkoppert +* @jmeridth @sutterj @zkoppert From a87cac5d4d21c40c65b4ee3f23fc175b4f16b9b5 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Fri, 7 Jun 2024 15:09:49 -0500 Subject: [PATCH 082/358] chore: standardize github action types - [x] use one line lists for types and branches - [x] fix formatting (random 2 space column on some of the workflows) - [x] add `edited` and `reopened` to some of the workflows - [x] fix issue with get_bool_env_var function and add tests (new file) Signed-off-by: jmeridth --- .github/workflows/auto-labeler.yml | 40 +++---- .github/workflows/linter.yaml | 2 +- .github/workflows/pr-title.yml | 5 +- .github/workflows/release.yml | 178 ++++++++++++++--------------- .github/workflows/scorecard.yml | 2 +- config.py | 20 ++-- test_config_get_bool.py | 81 +++++++++++++ 7 files changed, 204 insertions(+), 124 deletions(-) create mode 100644 test_config_get_bool.py diff --git a/.github/workflows/auto-labeler.yml b/.github/workflows/auto-labeler.yml index 9cb7f261..7ee19283 100644 --- a/.github/workflows/auto-labeler.yml +++ b/.github/workflows/auto-labeler.yml @@ -1,24 +1,24 @@ --- - name: Auto Labeler +name: Auto Labeler - on: - # pull_request_target event is required for autolabeler to support all PRs including forks - pull_request_target: - types: [opened, reopened, synchronize] +on: + # pull_request_target event is required for autolabeler to support all PRs including forks + pull_request_target: + types: [ opened, reopened, edited, synchronize ] - permissions: - contents: read +permissions: + contents: read - jobs: - main: - permissions: - contents: write - pull-requests: write - name: Auto label pull requests - runs-on: ubuntu-latest - steps: - - uses: release-drafter/release-drafter@3f0f87098bd6b5c5b9a36d49c41d998ea58f9348 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - config-name: release-drafter.yml +jobs: + main: + permissions: + contents: write + pull-requests: write + name: Auto label pull requests + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@3f0f87098bd6b5c5b9a36d49c41d998ea58f9348 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + config-name: release-drafter.yml diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index 1a8a28ab..2ff33736 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -3,7 +3,7 @@ name: Lint Code Base on: pull_request: - branches: main + branches: [ main ] permissions: contents: read diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index 50a04a83..3746ac85 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -4,10 +4,7 @@ name: "Lint PR Title" on: pull_request_target: - types: - - opened - - edited - - synchronize + types: [ opened, reopened, edited, synchronize ] permissions: contents: read diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1953deb..34b14656 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,94 +1,92 @@ --- - name: Release +name: Release - on: - workflow_dispatch: - pull_request_target: - types: - - closed - branches: - - main +on: + workflow_dispatch: + pull_request_target: + types: [ closed ] + branches: [ main ] - permissions: - contents: read +permissions: + contents: read - jobs: - create_release: - # release if - # manual deployment OR - # merged to main and labelled with release labels - if: | - (github.event_name == 'workflow_dispatch') || - (github.event.pull_request.merged == true && - (contains(github.event.pull_request.labels.*.name, 'breaking') || - contains(github.event.pull_request.labels.*.name, 'feature') || - contains(github.event.pull_request.labels.*.name, 'vuln') || - contains(github.event.pull_request.labels.*.name, 'release'))) - outputs: - full-tag: ${{ steps.release-drafter.outputs.tag_name }} - short-tag: ${{ steps.get_tag_name.outputs.SHORT_TAG }} - body: ${{ steps.release-drafter.outputs.body }} - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: read - steps: - - uses: release-drafter/release-drafter@3f0f87098bd6b5c5b9a36d49c41d998ea58f9348 - id: release-drafter - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - config-name: release-drafter.yml - publish: true - - name: Get the short tag - id: get_tag_name - run: | - short_tag=$(echo ${{ steps.release-drafter.outputs.tag_name }} | cut -d. -f1) - echo "SHORT_TAG=$short_tag" >> $GITHUB_OUTPUT - create_action_images: - needs: create_release - runs-on: ubuntu-latest - permissions: - packages: write - env: - REGISTRY: ghcr.io - IMAGE_NAME: github/issue_metrics # different than repo name (underscore instead of dash) - steps: - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb - - name: Log in to the Container registry - uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - - name: Push Docker Image - if: ${{ success() }} - uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 - with: - context: . - file: ./Dockerfile - push: true - tags: | - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.create_release.outputs.full-tag }} - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.create_release.outputs.short-tag }} - platforms: linux/amd64 - provenance: false - sbom: false - create_discussion: - needs: create_release - runs-on: ubuntu-latest - permissions: - discussions: write - steps: - - name: Create an announcement discussion for release - uses: abirismyname/create-discussion@6e6ef67e5eeb042343ef8b3d8d0f5d545cbdf024 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - title: ${{ needs.create_release.outputs.full-tag }} - body: ${{ needs.create_release.outputs.body }} - repository-id: ${{ secrets.RELEASE_DISCUSSION_REPOSITORY_ID }} - category-id: ${{ secrets.RELEASE_DISCUSSION_CATEGORY_ID }} +jobs: + create_release: + # release if + # manual deployment OR + # merged to main and labelled with release labels + if: | + (github.event_name == 'workflow_dispatch') || + (github.event.pull_request.merged == true && + (contains(github.event.pull_request.labels.*.name, 'breaking') || + contains(github.event.pull_request.labels.*.name, 'feature') || + contains(github.event.pull_request.labels.*.name, 'vuln') || + contains(github.event.pull_request.labels.*.name, 'release'))) + outputs: + full-tag: ${{ steps.release-drafter.outputs.tag_name }} + short-tag: ${{ steps.get_tag_name.outputs.SHORT_TAG }} + body: ${{ steps.release-drafter.outputs.body }} + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: read + steps: + - uses: release-drafter/release-drafter@3f0f87098bd6b5c5b9a36d49c41d998ea58f9348 + id: release-drafter + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + config-name: release-drafter.yml + publish: true + - name: Get the short tag + id: get_tag_name + run: | + short_tag=$(echo ${{ steps.release-drafter.outputs.tag_name }} | cut -d. -f1) + echo "SHORT_TAG=$short_tag" >> $GITHUB_OUTPUT + create_action_images: + needs: create_release + runs-on: ubuntu-latest + permissions: + packages: write + env: + REGISTRY: ghcr.io + IMAGE_NAME: github/issue_metrics # different than repo name (underscore instead of dash) + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb + - name: Log in to the Container registry + uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + - name: Push Docker Image + if: ${{ success() }} + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 + with: + context: . + file: ./Dockerfile + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.create_release.outputs.full-tag }} + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.create_release.outputs.short-tag }} + platforms: linux/amd64 + provenance: false + sbom: false + create_discussion: + needs: create_release + runs-on: ubuntu-latest + permissions: + discussions: write + steps: + - name: Create an announcement discussion for release + uses: abirismyname/create-discussion@6e6ef67e5eeb042343ef8b3d8d0f5d545cbdf024 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + title: ${{ needs.create_release.outputs.full-tag }} + body: ${{ needs.create_release.outputs.body }} + repository-id: ${{ secrets.RELEASE_DISCUSSION_REPOSITORY_ID }} + category-id: ${{ secrets.RELEASE_DISCUSSION_CATEGORY_ID }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index e3784860..c5158aee 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -11,7 +11,7 @@ on: schedule: - cron: '29 11 * * 6' push: - branches: ["main"] + branches: [ main ] permissions: read-all diff --git a/config.py b/config.py index 30e91aa7..d6c2c752 100644 --- a/config.py +++ b/config.py @@ -102,16 +102,20 @@ def __repr__(self): ) -def get_bool_env_var(env_var_name: str) -> bool: +def get_bool_env_var(env_var_name: str, default: bool = False) -> bool: """Get a boolean environment variable. Args: env_var_name: The name of the environment variable to retrieve. + default: The default value to return if the environment variable is not set. Returns: The value of the environment variable as a boolean. """ - return os.environ.get(env_var_name, "").strip().lower() == "true" + ev = os.environ.get(env_var_name, "") + if ev == "" and default: + return default + return ev.strip().lower() == "true" def get_int_env_var(env_var_name: str) -> int | None: @@ -177,12 +181,12 @@ def get_env_vars(test: bool = False) -> EnvVars: ignore_users_list = ignore_users.split(",") # Hidden columns - hide_author = get_bool_env_var("HIDE_AUTHOR") - hide_label_metrics = get_bool_env_var("HIDE_LABEL_METRICS") - hide_time_to_answer = get_bool_env_var("HIDE_TIME_TO_ANSWER") - hide_time_to_close = get_bool_env_var("HIDE_TIME_TO_CLOSE") - hide_time_to_first_response = get_bool_env_var("HIDE_TIME_TO_FIRST_RESPONSE") - enable_mentor_count = get_bool_env_var("ENABLE_MENTOR_COUNT") + hide_author = get_bool_env_var("HIDE_AUTHOR", False) + hide_label_metrics = get_bool_env_var("HIDE_LABEL_METRICS", False) + hide_time_to_answer = get_bool_env_var("HIDE_TIME_TO_ANSWER", False) + hide_time_to_close = get_bool_env_var("HIDE_TIME_TO_CLOSE", False) + hide_time_to_first_response = get_bool_env_var("HIDE_TIME_TO_FIRST_RESPONSE", False) + enable_mentor_count = get_bool_env_var("ENABLE_MENTOR_COUNT", False) min_mentor_comments = os.getenv("MIN_MENTOR_COMMENTS", "10") max_comments_eval = os.getenv("MAX_COMMENTS_EVAL", "20") heavily_involved_cutoff = os.getenv("HEAVILY_INVOLVED_CUTOFF", "3") diff --git a/test_config_get_bool.py b/test_config_get_bool.py new file mode 100644 index 00000000..c19f7ab4 --- /dev/null +++ b/test_config_get_bool.py @@ -0,0 +1,81 @@ +"""Test the get_get_bool_env_var function""" + +import os +import unittest +from unittest.mock import patch + +from config import get_bool_env_var + + +class TestEnv(unittest.TestCase): + """Test the get_bool_env_var function""" + + @patch.dict( + os.environ, + { + "TEST_BOOL": "true", + }, + clear=True, + ) + def test_get_bool_env_var_that_exists_and_is_true(self): + """Test that gets a boolean environment variable that exists and is true""" + result = get_bool_env_var("TEST_BOOL", False) + self.assertTrue(result) + + @patch.dict( + os.environ, + { + "TEST_BOOL": "false", + }, + clear=True, + ) + def test_get_bool_env_var_that_exists_and_is_false(self): + """Test that gets a boolean environment variable that exists and is false""" + result = get_bool_env_var("TEST_BOOL", False) + self.assertFalse(result) + + @patch.dict( + os.environ, + { + "TEST_BOOL": "nope", + }, + clear=True, + ) + def test_get_bool_env_var_that_exists_and_is_false_due_to_invalid_value(self): + """Test that gets a boolean environment variable that exists and is false + due to an invalid value + """ + result = get_bool_env_var("TEST_BOOL", False) + self.assertFalse(result) + + @patch.dict( + os.environ, + { + "TEST_BOOL": "false", + }, + clear=True, + ) + def test_get_bool_env_var_that_does_not_exist_and_default_value_returns_true(self): + """Test that gets a boolean environment variable that does not exist + and default value returns: true + """ + result = get_bool_env_var("DOES_NOT_EXIST", True) + self.assertTrue(result) + + @patch.dict( + os.environ, + { + "TEST_BOOL": "true", + }, + clear=True, + ) + def test_get_bool_env_var_that_does_not_exist_and_default_value_returns_false(self): + """Test that gets a boolean environment variable that does not exist + and default value returns: false + """ + result = get_bool_env_var("DOES_NOT_EXIST", False) + self.assertFalse(result) + + +if __name__ == "__main__": + unittest.main() From 3d674bca3e9cd07f137168ca54c3fa17e1aad135 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Jun 2024 17:39:22 +0000 Subject: [PATCH 083/358] chore(deps): bump the dependencies group with 2 updates Bumps the dependencies group with 2 updates: [pylint](https://github.com/pylint-dev/pylint) and [pytest](https://github.com/pytest-dev/pytest). Updates `pylint` from 3.2.2 to 3.2.3 - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v3.2.2...v3.2.3) Updates `pytest` from 8.2.1 to 8.2.2 - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.2.1...8.2.2) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index cc2ca8ca..b9a1937f 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,8 +2,8 @@ black==24.4.2 flake8==7.0.0 mypy==1.10.0 mypy-extensions==1.0.0 -pylint==3.2.2 -pytest==8.2.1 +pylint==3.2.3 +pytest==8.2.2 pytest-cov==5.0.0 types-pytz==2024.1.0.20240417 types-requests==2.32.0.20240602 From 4b9d4c70fefc998523eacb1360ef516753d95bb7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Jun 2024 17:42:40 +0000 Subject: [PATCH 084/358] chore(deps): bump the dependencies group with 3 updates Bumps the dependencies group with 3 updates: [super-linter/super-linter](https://github.com/super-linter/super-linter), [docker/build-push-action](https://github.com/docker/build-push-action) and [github/codeql-action](https://github.com/github/codeql-action). Updates `super-linter/super-linter` from 6.5.1 to 6.6.0 - [Release notes](https://github.com/super-linter/super-linter/releases) - [Changelog](https://github.com/super-linter/super-linter/blob/main/CHANGELOG.md) - [Commits](https://github.com/super-linter/super-linter/compare/5b638caee6ba65e25e07143887b669a1233847a0...88ea3923a7e1f89dd485d079f6eb5f5e8f937589) Updates `docker/build-push-action` from 5.3.0 to 5.4.0 - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/2cdde995de11925a030ce8070c3d77a52ffcf1c0...ca052bb54ab0790a636c9b5f226502c73d547a25) Updates `github/codeql-action` from 3.25.7 to 3.25.8 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/f079b8493333aace61c81488f8bd40919487bd9f...2e230e8fe0ad3a14a340ad0815ddb96d599d2aff) --- updated-dependencies: - dependency-name: super-linter/super-linter dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/linter.yaml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/scorecard.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index 2ff33736..4874d344 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -28,7 +28,7 @@ jobs: python -m pip install --upgrade pip pip install -r requirements.txt -r requirements-test.txt - name: Lint Code Base - uses: super-linter/super-linter@5b638caee6ba65e25e07143887b669a1233847a0 + uses: super-linter/super-linter@88ea3923a7e1f89dd485d079f6eb5f5e8f937589 env: DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 34b14656..1b8cf42f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,7 +63,7 @@ jobs: - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Push Docker Image if: ${{ success() }} - uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 + uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 with: context: . file: ./Dockerfile diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index c5158aee..13a8a3d7 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@f079b8493333aace61c81488f8bd40919487bd9f # v3.24.9 + uses: github/codeql-action/upload-sarif@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.24.9 with: sarif_file: results.sarif From 5e6d889332629ae232e21f0ae7ff7cef72af9ec8 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Mon, 10 Jun 2024 15:49:55 -0700 Subject: [PATCH 085/358] fix: issue counts incorrect when HIDE_TIME_TO_CLOSE is True Signed-off-by: Zack Koppert --- issue_metrics.py | 40 +++++---- test_issue_metrics.py | 175 +++++++++++++++++++++++++++++++++++++++- test_markdown_writer.py | 4 +- 3 files changed, 192 insertions(+), 27 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index e5024a42..0d7645fc 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -193,14 +193,12 @@ def get_per_issue_metrics( ) if env_vars.hide_time_to_answer is False: issue_with_metrics.time_to_answer = measure_time_to_answer(issue) - if env_vars.hide_time_to_close is False: - if issue["closedAt"]: - issue_with_metrics.time_to_close = measure_time_to_close( - None, issue - ) - num_issues_closed += 1 - else: - num_issues_open += 1 + if env_vars.hide_time_to_close is False and issue["closedAt"]: + issue_with_metrics.time_to_close = measure_time_to_close(None, issue) + if issue["closedAt"]: + num_issues_closed += 1 + else: + num_issues_open += 1 else: issue_with_metrics = IssueWithMetrics( issue.title, # type: ignore @@ -236,19 +234,19 @@ def get_per_issue_metrics( ) if labels and env_vars.hide_label_metrics is False: issue_with_metrics.label_metrics = get_label_metrics(issue, labels) - if env_vars.hide_time_to_close is False: - if issue.state == "closed": # type: ignore - if pull_request: - issue_with_metrics.time_to_close = measure_time_to_merge( - pull_request, ready_for_review_at - ) - else: - issue_with_metrics.time_to_close = measure_time_to_close( - issue, None - ) - num_issues_closed += 1 - elif issue.state == "open": # type: ignore - num_issues_open += 1 + if env_vars.hide_time_to_close is False and issue.state == "closed": # type: ignore + if pull_request: + issue_with_metrics.time_to_close = measure_time_to_merge( + pull_request, ready_for_review_at + ) + else: + issue_with_metrics.time_to_close = measure_time_to_close( + issue, None + ) + if issue.state == "closed": # type: ignore + num_issues_closed += 1 + elif issue.state == "open": # type: ignore + num_issues_open += 1 issues_with_metrics.append(issue_with_metrics) return issues_with_metrics, num_issues_open, num_issues_closed diff --git a/test_issue_metrics.py b/test_issue_metrics.py index 1cf6d1d9..5059a88d 100644 --- a/test_issue_metrics.py +++ b/test_issue_metrics.py @@ -175,10 +175,131 @@ class TestGetPerIssueMetrics(unittest.TestCase): @patch.dict( os.environ, - {"GH_TOKEN": "test_token", "SEARCH_QUERY": "is:issue is:open repo:user/repo"}, + { + "GH_TOKEN": "test_token", + "SEARCH_QUERY": "is:issue is:open repo:user/repo", + "HIDE_AUTHOR": "true", + "HIDE_LABEL_METRICS": "true", + "HIDE_TIME_TO_ANSWER": "true", + "HIDE_TIME_TO_CLOSE": "true", + "HIDE_TIME_TO_FIRST_RESPONSE": "true", + }, + ) + def test_get_per_issue_metrics_with_hide_envs(self): + """ + Test that the function correctly calculates the metrics for + a list of GitHub issues where HIDE_* envs are set true + """ + + # Create mock data + mock_issue1 = MagicMock( + title="Issue 1", + html_url="https://github.com/user/repo/issues/1", + author="alice", + state="open", + comments=1, + created_at="2023-01-01T00:00:00Z", + ) + + mock_comment1 = MagicMock() + mock_comment1.created_at = datetime.fromisoformat("2023-01-02T00:00:00Z") + mock_issue1.issue.comments.return_value = [mock_comment1] + mock_issue1.issue.pull_request_urls = None + + mock_issue2 = MagicMock( + title="Issue 2", + html_url="https://github.com/user/repo/issues/2", + author="bob", + state="closed", + comments=1, + created_at="2023-01-01T00:00:00Z", + closed_at="2023-01-04T00:00:00Z", + ) + + mock_comment2 = MagicMock() + mock_comment2.created_at = datetime.fromisoformat("2023-01-03T00:00:00Z") + mock_issue2.issue.comments.return_value = [mock_comment2] + mock_issue2.issue.pull_request_urls = None + + issues = [ + mock_issue1, + mock_issue2, + ] + + # Call the function and check the result + with unittest.mock.patch( # type:ignore + "issue_metrics.measure_time_to_first_response", + measure_time_to_first_response, + ), unittest.mock.patch( # type:ignore + "issue_metrics.measure_time_to_close", measure_time_to_close + ): + ( + result_issues_with_metrics, + result_num_issues_open, + result_num_issues_closed, + ) = get_per_issue_metrics( + issues, + env_vars=get_env_vars(test=True), + ) + expected_issues_with_metrics = [ + IssueWithMetrics( + "Issue 1", + "https://github.com/user/repo/issues/1", + "alice", + None, + None, + None, + None, + ), + IssueWithMetrics( + "Issue 2", + "https://github.com/user/repo/issues/2", + "bob", + None, + None, + None, + None, + ), + ] + expected_num_issues_open = 1 + expected_num_issues_closed = 1 + self.assertEqual(result_num_issues_open, expected_num_issues_open) + self.assertEqual(result_num_issues_closed, expected_num_issues_closed) + self.assertEqual( + result_issues_with_metrics[0].time_to_first_response, + expected_issues_with_metrics[0].time_to_first_response, + ) + self.assertEqual( + result_issues_with_metrics[0].time_to_close, + expected_issues_with_metrics[0].time_to_close, + ) + self.assertEqual( + result_issues_with_metrics[1].time_to_first_response, + expected_issues_with_metrics[1].time_to_first_response, + ) + self.assertEqual( + result_issues_with_metrics[1].time_to_close, + expected_issues_with_metrics[1].time_to_close, + ) + + @patch.dict( + os.environ, + { + "GH_TOKEN": "test_token", + "SEARCH_QUERY": "is:issue is:open repo:user/repo", + "HIDE_AUTHOR": "false", + "HIDE_LABEL_METRICS": "false", + "HIDE_TIME_TO_ANSWER": "false", + "HIDE_TIME_TO_CLOSE": "false", + "HIDE_TIME_TO_FIRST_RESPONSE": "false", + }, ) - def test_get_per_issue_metrics(self): - """Test that the function correctly calculates the metrics for a list of GitHub issues.""" + def test_get_per_issue_metrics_without_hide_envs(self): + """ + Test that the function correctly calculates the metrics for + a list of GitHub issues where HIDE_* envs are set false + """ + # Create mock data mock_issue1 = MagicMock( title="Issue 1", @@ -225,7 +346,10 @@ def test_get_per_issue_metrics(self): result_issues_with_metrics, result_num_issues_open, result_num_issues_closed, - ) = get_per_issue_metrics(issues, env_vars=get_env_vars(test=True)) + ) = get_per_issue_metrics( + issues, + env_vars=get_env_vars(test=True), + ) expected_issues_with_metrics = [ IssueWithMetrics( "Issue 1", @@ -337,6 +461,49 @@ def test_get_per_issue_metrics_with_discussion(self): self.assertEqual(metrics[0][1].time_to_close, timedelta(days=6)) self.assertEqual(metrics[0][1].time_to_first_response, timedelta(days=2)) + @patch.dict( + os.environ, + { + "GH_TOKEN": "test_token", + "SEARCH_QUERY": "is:issue is:open repo:user/repo", + "HIDE_AUTHOR": "true", + "HIDE_LABEL_METRICS": "true", + "HIDE_TIME_TO_ANSWER": "true", + "HIDE_TIME_TO_CLOSE": "true", + "HIDE_TIME_TO_FIRST_RESPONSE": "true", + }, + ) + def test_get_per_issue_metrics_with_discussion_with_hide_envs(self): + """ + Test that the function correctly calculates + the metrics for a list of GitHub issues with discussions + and HIDE_* env vars set to True + """ + + issues = [self.issue1, self.issue2] + metrics = get_per_issue_metrics( + issues, discussions=True, env_vars=get_env_vars(test=True) + ) + + # get_per_issue_metrics returns a tuple of + # (issues_with_metrics, num_issues_open, num_issues_closed) + self.assertEqual(len(metrics), 3) + + # Check that the metrics are correct, 0 issues open, 2 issues closed + self.assertEqual(metrics[1], 0) + self.assertEqual(metrics[2], 2) + + # Check that the issues_with_metrics has 2 issues in it + self.assertEqual(len(metrics[0]), 2) + + # Check that the issues_with_metrics has the correct metrics, + self.assertEqual(metrics[0][0].time_to_answer, None) + self.assertEqual(metrics[0][0].time_to_close, None) + self.assertEqual(metrics[0][0].time_to_first_response, None) + self.assertEqual(metrics[0][1].time_to_answer, None) + self.assertEqual(metrics[0][1].time_to_close, None) + self.assertEqual(metrics[0][1].time_to_first_response, None) + if __name__ == "__main__": unittest.main() diff --git a/test_markdown_writer.py b/test_markdown_writer.py index 053dc0a0..3ebb692f 100644 --- a/test_markdown_writer.py +++ b/test_markdown_writer.py @@ -297,7 +297,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): "label1": timedelta(days=1), } num_issues_opened = 2 - num_issues_closed = 1 + num_issues_closed = 2 num_mentor_count = 5 # Call the function @@ -323,7 +323,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): "| Metric | Count |\n" "| --- | ---: |\n" "| Number of items that remain open | 2 |\n" - "| Number of items closed | 1 |\n" + "| Number of items closed | 2 |\n" "| Number of most active mentors | 5 |\n" "| Total number of items created | 2 |\n\n" "| Title | URL | Author |\n" From a987f816c64cf8a7ca9265332d558a7cb4ca27b9 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 11 Jun 2024 08:59:44 -0700 Subject: [PATCH 086/358] chore: readable and pythonic logic Co-authored-by: jmeridth --- issue_metrics.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index 0d7645fc..60cc3ea8 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -193,10 +193,10 @@ def get_per_issue_metrics( ) if env_vars.hide_time_to_answer is False: issue_with_metrics.time_to_answer = measure_time_to_answer(issue) - if env_vars.hide_time_to_close is False and issue["closedAt"]: - issue_with_metrics.time_to_close = measure_time_to_close(None, issue) if issue["closedAt"]: num_issues_closed += 1 + if not env_vars.hide_time_to_close: + issue_with_metrics.time_to_close = measure_time_to_close(None, issue) else: num_issues_open += 1 else: @@ -234,17 +234,17 @@ def get_per_issue_metrics( ) if labels and env_vars.hide_label_metrics is False: issue_with_metrics.label_metrics = get_label_metrics(issue, labels) - if env_vars.hide_time_to_close is False and issue.state == "closed": # type: ignore - if pull_request: - issue_with_metrics.time_to_close = measure_time_to_merge( - pull_request, ready_for_review_at - ) - else: - issue_with_metrics.time_to_close = measure_time_to_close( - issue, None - ) if issue.state == "closed": # type: ignore num_issues_closed += 1 + if not env_vars.hide_time_to_close: + if pull_request: + issue_with_metrics.time_to_close = measure_time_to_merge( + pull_request, ready_for_review_at + ) + else: + issue_with_metrics.time_to_close = measure_time_to_close( + issue, None + ) elif issue.state == "open": # type: ignore num_issues_open += 1 issues_with_metrics.append(issue_with_metrics) From 7e2cae419502993fd25c129335fa1f6240dbe65c Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 11 Jun 2024 09:27:24 -0700 Subject: [PATCH 087/358] chore: linting Signed-off-by: Zack Koppert --- issue_metrics.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/issue_metrics.py b/issue_metrics.py index 60cc3ea8..dfe57f24 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -196,7 +196,9 @@ def get_per_issue_metrics( if issue["closedAt"]: num_issues_closed += 1 if not env_vars.hide_time_to_close: - issue_with_metrics.time_to_close = measure_time_to_close(None, issue) + issue_with_metrics.time_to_close = measure_time_to_close( + None, issue + ) else: num_issues_open += 1 else: From 7c09c70ca89832d6155b2c0ef363e75b0615e601 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 11 Jun 2024 10:48:32 -0700 Subject: [PATCH 088/358] feat: Add option to hide Number of items closed metric Signed-off-by: Zack Koppert --- .env-example | 1 + README.md | 1 + config.py | 8 +++- issue_metrics.py | 2 + markdown_writer.py | 7 +++- test_config.py | 89 +++++++++++++++++++++++++++++++---------- test_markdown_writer.py | 2 +- 7 files changed, 85 insertions(+), 25 deletions(-) diff --git a/.env-example b/.env-example index f50b62c4..c905ee3d 100644 --- a/.env-example +++ b/.env-example @@ -4,6 +4,7 @@ GH_APP_PRIVATE_KEY="" GH_ENTERPRISE_URL = "" GH_TOKEN = "" HIDE_AUTHOR = "false" +HIDE_ITEMS_CLOSED_COUNT="false" HIDE_LABEL_METRICS = "false" HIDE_TIME_TO_ANSWER = "false" HIDE_TIME_TO_CLOSE = "false" diff --git a/README.md b/README.md index 456a781d..e4e0da90 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ This action can be configured to authenticate with GitHub App Installation or Pe | `HEAVILY_INVOLVED_CUTOFF` | False | 3 | Cutoff after which a mentor's comments in one issue are no longer counted against their total score | | `LABELS_TO_MEASURE` | False | `""` | A comma separated list of labels to measure how much time the label is applied. If not provided, no labels durations will be measured. Not compatible with discussions at this time. | | `SEARCH_QUERY` | True | `""` | The query by which you can filter issues/PRs which must contain a `repo:`, `org:`, `owner:`, or a `user:` entry. For discussions, include `type:discussions` in the query. | +| `HIDE_ITEMS_CLOSED_COUNT` | False | False | If set to `true`, the number of items closed metric will not be displayed in the generated Markdown file. | ## Further Documentation diff --git a/config.py b/config.py index d6c2c752..5186d441 100644 --- a/config.py +++ b/config.py @@ -28,6 +28,7 @@ class EnvVars: gh_token (str | None): GitHub personal access token (PAT) for API authentication ghe (str): The GitHub Enterprise URL to use for authentication hide_author (bool): If true, the author's information is hidden in the output + hide_items_closed_count (bool): If true, the number of items closed metric is hidden in the output hide_label_metrics (bool): If true, the label metrics are hidden in the output hide_time_to_answer (bool): If true, the time to answer discussions is hidden in the output hide_time_to_close (bool): If true, the time to close metric is hidden in the output @@ -49,6 +50,7 @@ def __init__( gh_token: str | None, ghe: str | None, hide_author: bool, + hide_items_closed_count: bool, hide_label_metrics: bool, hide_time_to_answer: bool, hide_time_to_close: bool, @@ -69,6 +71,7 @@ def __init__( self.ignore_users = ignore_user self.labels_to_measure = labels_to_measure self.hide_author = hide_author + self.hide_items_closed_count = hide_items_closed_count self.hide_label_metrics = hide_label_metrics self.hide_time_to_answer = hide_time_to_answer self.hide_time_to_close = hide_time_to_close @@ -88,6 +91,7 @@ def __repr__(self): f"{self.gh_token}," f"{self.ghe}," f"{self.hide_author}," + f"{self.hide_items_closed_count})," f"{self.hide_label_metrics}," f"{self.hide_time_to_answer}," f"{self.hide_time_to_close}," @@ -98,7 +102,7 @@ def __repr__(self): f"{self.min_mentor_comments}," f"{self.max_comments_eval}," f"{self.heavily_involved_cutoff}," - f"{self.search_query})" + f"{self.search_query}" ) @@ -182,6 +186,7 @@ def get_env_vars(test: bool = False) -> EnvVars: # Hidden columns hide_author = get_bool_env_var("HIDE_AUTHOR", False) + hide_items_closed_count = get_bool_env_var("HIDE_ITEMS_CLOSED_COUNT", False) hide_label_metrics = get_bool_env_var("HIDE_LABEL_METRICS", False) hide_time_to_answer = get_bool_env_var("HIDE_TIME_TO_ANSWER", False) hide_time_to_close = get_bool_env_var("HIDE_TIME_TO_CLOSE", False) @@ -198,6 +203,7 @@ def get_env_vars(test: bool = False) -> EnvVars: gh_token, ghe, hide_author, + hide_items_closed_count, hide_label_metrics, hide_time_to_answer, hide_time_to_close, diff --git a/issue_metrics.py b/issue_metrics.py index e5024a42..7a6bde43 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -305,6 +305,7 @@ def main(): search_query = env_vars.search_query token = env_vars.gh_token ignore_users = env_vars.ignore_users + hide_items_closed_count = env_vars.hide_items_closed_count gh_app_id = env_vars.gh_app_id gh_app_installation_id = env_vars.gh_app_installation_id @@ -412,6 +413,7 @@ def main(): num_mentor_count, labels, search_query, + hide_items_closed_count, ) max_char_count = 65535 diff --git a/markdown_writer.py b/markdown_writer.py index 0b587aef..5e3e3008 100644 --- a/markdown_writer.py +++ b/markdown_writer.py @@ -84,6 +84,7 @@ def write_to_markdown( labels=None, search_query=None, hide_label_metrics=False, + hide_items_closed_count=False, ) -> None: """Write the issues with metrics to a markdown file. @@ -102,6 +103,7 @@ def write_to_markdown( labels (List[str]): A list of the labels that are used in the issues. search_query (str): The search query used to find the issues. hide_label_metrics (bool): Represents whether the user has chosen to hide label metrics in the output + hide_items_closed_count (bool): Represents whether the user has chosen to hide the number of items closed Returns: None. @@ -135,6 +137,7 @@ def write_to_markdown( columns, file, hide_label_metrics, + hide_items_closed_count, ) # Write second table with individual issue/pr/discussion metrics @@ -193,6 +196,7 @@ def write_overall_metrics_tables( columns, file, hide_label_metrics, + hide_items_closed_count=False, ): """Write the overall metrics tables to the markdown file.""" if ( @@ -250,6 +254,7 @@ def write_overall_metrics_tables( file.write("| Metric | Count |\n") file.write("| --- | ---: |\n") file.write(f"| Number of items that remain open | {num_issues_opened} |\n") - file.write(f"| Number of items closed | {num_issues_closed} |\n") + if not hide_items_closed_count: + file.write(f"| Number of items closed | {num_issues_closed} |\n") file.write(f"| Number of most active mentors | {num_mentor_count} |\n") file.write(f"| Total number of items created | {len(issues_with_metrics)} |\n\n") diff --git a/test_config.py b/test_config.py index d2f1bd15..6c2abefa 100644 --- a/test_config.py +++ b/test_config.py @@ -72,6 +72,7 @@ def setUp(self): "GH_TOKEN", "GHE", "HIDE_AUTHOR", + "HIDE_ITEMS_CLOSED_COUNT", "HIDE_LABEL_METRICS", "HIDE_TIME_TO_ANSWER", "HIDE_TIME_TO_CLOSE", @@ -93,6 +94,7 @@ def setUp(self): "GH_TOKEN": "", "GH_ENTERPRISE_URL": "", "HIDE_AUTHOR": "", + "HIDE_ITEMS_CLOSED_COUNT": "false", "HIDE_LABEL_METRICS": "", "HIDE_TIME_TO_ANSWER": "", "HIDE_TIME_TO_CLOSE": "", @@ -116,6 +118,7 @@ def test_get_env_vars_with_github_app(self): False, False, False, + False, [], [], False, @@ -136,6 +139,7 @@ def test_get_env_vars_with_github_app(self): "GH_ENTERPRISE_URL": "", "GH_TOKEN": TOKEN, "HIDE_AUTHOR": "", + "HIDE_ITEMS_CLOSED_COUNT": "false", "HIDE_LABEL_METRICS": "", "HIDE_TIME_TO_ANSWER": "", "HIDE_TIME_TO_CLOSE": "", @@ -159,6 +163,7 @@ def test_get_env_vars_with_token(self): False, False, False, + False, [], [], False, @@ -170,6 +175,41 @@ def test_get_env_vars_with_token(self): result = get_env_vars(True) self.assertEqual(str(result), str(expected_result)) + @patch.dict( + os.environ, + { + "GH_APP_ID": "", + "GH_APP_INSTALLATION_ID": "", + "GH_APP_PRIVATE_KEY": "", + "GH_TOKEN": "", + "SEARCH_QUERY": SEARCH_QUERY, + "HIDE_ITEMS_CLOSED_COUNT": "false", + }, + clear=True, + ) + def test_get_env_vars_missing_token(self): + """Test that an error is raised if the TOKEN environment variables is not set""" + with self.assertRaises(ValueError): + get_env_vars(True) + + @patch.dict( + os.environ, + { + "GH_APP_ID": "", + "GH_APP_INSTALLATION_ID": "", + "GH_APP_PRIVATE_KEY": "", + "GH_TOKEN": TOKEN, + "SEARCH_QUERY": "", + "HIDE_ITEMS_CLOSED_COUNT": "false", + }, + clear=True, + ) + def test_get_env_vars_missing_query(self): + """Test that an error is raised if the SEARCH_QUERY environment variable is not set.""" + + with self.assertRaises(ValueError): + get_env_vars(True) + @patch.dict( os.environ, { @@ -179,6 +219,7 @@ def test_get_env_vars_with_token(self): "GH_TOKEN": TOKEN, "GH_ENTERPRISE_URL": "", "HIDE_AUTHOR": "true", + "HIDE_ITEMS_CLOSED_COUNT": "true", "HIDE_LABEL_METRICS": "true", "HIDE_TIME_TO_ANSWER": "true", "HIDE_TIME_TO_CLOSE": "true", @@ -201,6 +242,7 @@ def test_get_env_vars_optional_values(self): True, True, True, + True, [], ["waiting-for-review", "waiting-for-manager"], False, @@ -218,32 +260,35 @@ def test_get_env_vars_optional_values(self): "GH_APP_ID": "", "GH_APP_INSTALLATION_ID": "", "GH_APP_PRIVATE_KEY": "", - "GH_TOKEN": "", + "GH_TOKEN": "TOKEN", "SEARCH_QUERY": SEARCH_QUERY, }, clear=True, ) - def test_get_env_vars_missing_token(self): - """Test that an error is raised if the TOKEN environment variables is not set""" - with self.assertRaises(ValueError): - get_env_vars(True) - - @patch.dict( - os.environ, - { - "GH_APP_ID": "", - "GH_APP_INSTALLATION_ID": "", - "GH_APP_PRIVATE_KEY": "", - "GH_TOKEN": TOKEN, - "SEARCH_QUERY": "", - }, - clear=True, - ) - def test_get_env_vars_missing_query(self): - """Test that an error is raised if the SEARCH_QUERY environment variable is not set.""" - - with self.assertRaises(ValueError): - get_env_vars(True) + def test_get_env_vars_optionals_are_defaulted(self): + """Test that optional values are set to their default values if not provided""" + expected_result = EnvVars( + None, + None, + b"", + "TOKEN", + "", + False, + False, + False, + False, + False, + False, + [], + [], + False, + "10", + "20", + "3", + SEARCH_QUERY, + ) + result = get_env_vars(True) + self.assertEqual(str(result), str(expected_result)) if __name__ == "__main__": diff --git a/test_markdown_writer.py b/test_markdown_writer.py index 053dc0a0..d4aaad5e 100644 --- a/test_markdown_writer.py +++ b/test_markdown_writer.py @@ -313,6 +313,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): labels=["label1"], search_query="repo:user/repo is:issue", hide_label_metrics=True, + hide_items_closed_count=True, ) # Check that the function writes the correct markdown file @@ -323,7 +324,6 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): "| Metric | Count |\n" "| --- | ---: |\n" "| Number of items that remain open | 2 |\n" - "| Number of items closed | 1 |\n" "| Number of most active mentors | 5 |\n" "| Total number of items created | 2 |\n\n" "| Title | URL | Author |\n" From 1ffc7ca6ad7b8d15bb244ffd2e6b1f788bd979f4 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Wed, 12 Jun 2024 11:29:27 -0700 Subject: [PATCH 089/358] chore: proper order for env vars --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e4e0da90..241fc774 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,7 @@ This action can be configured to authenticate with GitHub App Installation or Pe |-------------------------------|----------|---------|-------------| | `GH_ENTERPRISE_URL` | False | `""` | URL of GitHub Enterprise instance to use for auth instead of github.com | | `HIDE_AUTHOR` | False | False | If set to `true`, the author will not be displayed in the generated Markdown file. | +| `HIDE_ITEMS_CLOSED_COUNT` | False | False | If set to `true`, the number of items closed metric will not be displayed in the generated Markdown file. | | `HIDE_LABEL_METRICS` | False | False | If set to `true`, the time in label metrics will not be displayed in the generated Markdown file. | | `HIDE_TIME_TO_ANSWER` | False | False | If set to `true`, the time to answer a discussion will not be displayed in the generated Markdown file. | | `HIDE_TIME_TO_CLOSE` | False | False | If set to `true`, the time to close will not be displayed in the generated Markdown file. | @@ -154,7 +155,6 @@ This action can be configured to authenticate with GitHub App Installation or Pe | `HEAVILY_INVOLVED_CUTOFF` | False | 3 | Cutoff after which a mentor's comments in one issue are no longer counted against their total score | | `LABELS_TO_MEASURE` | False | `""` | A comma separated list of labels to measure how much time the label is applied. If not provided, no labels durations will be measured. Not compatible with discussions at this time. | | `SEARCH_QUERY` | True | `""` | The query by which you can filter issues/PRs which must contain a `repo:`, `org:`, `owner:`, or a `user:` entry. For discussions, include `type:discussions` in the query. | -| `HIDE_ITEMS_CLOSED_COUNT` | False | False | If set to `true`, the number of items closed metric will not be displayed in the generated Markdown file. | ## Further Documentation From ffe608fa77ef2b6b07b61ef31d3a8371607497b1 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Thu, 13 Jun 2024 15:06:19 -0700 Subject: [PATCH 090/358] chore: pin shas in Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9620d364..318e6498 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.12-slim +FROM python:3.12-slim@sha256:e3ae8cf03c4f0abbfef13a8147478a7cd92798a94fa729a36a185d9106cbae32 LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From 22e9277ac699f5fcde5f1db839ccb87e18f34b1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 17:16:07 +0000 Subject: [PATCH 091/358] chore(deps): bump flake8 from 7.0.0 to 7.1.0 in the dependencies group Bumps the dependencies group with 1 update: [flake8](https://github.com/pycqa/flake8). Updates `flake8` from 7.0.0 to 7.1.0 - [Commits](https://github.com/pycqa/flake8/compare/7.0.0...7.1.0) --- updated-dependencies: - dependency-name: flake8 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index b9a1937f..b5e62471 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,5 +1,5 @@ black==24.4.2 -flake8==7.0.0 +flake8==7.1.0 mypy==1.10.0 mypy-extensions==1.0.0 pylint==3.2.3 From 8874c109428512bfadf2da2bb351a6c2b122c4c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 17:16:13 +0000 Subject: [PATCH 092/358] chore(deps): bump numpy from 1.26.4 to 2.0.0 Bumps [numpy](https://github.com/numpy/numpy) from 1.26.4 to 2.0.0. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v1.26.4...v2.0.0) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a6f68343..f6f4dbcf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ github3.py==4.0.1 -numpy==1.26.4 +numpy==2.0.0 python-dotenv==1.0.1 pytz==2024.1 requests==2.32.3 From 688a33c83401768ad685db9047d57e3f9f8b702d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 18:02:36 +0000 Subject: [PATCH 093/358] chore(deps): bump the dependencies group with 2 updates Bumps the dependencies group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [github/codeql-action](https://github.com/github/codeql-action). Updates `actions/checkout` from 4.1.6 to 4.1.7 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/a5ac7e51b41094c92402da3b24376905380afc29...692973e3d937129bcbf40652eb9f2f61becf3332) Updates `github/codeql-action` from 3.25.8 to 3.25.10 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/2e230e8fe0ad3a14a340ad0815ddb96d599d2aff...23acc5c183826b7a8a97bce3cecc52db901f8251) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/docker-image.yml | 2 +- .github/workflows/linter.yaml | 2 +- .github/workflows/major-version-updater.yml | 2 +- .github/workflows/python-package.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/scorecard.yml | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index f81f4477..6da5e769 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -14,6 +14,6 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Build the Docker image run: docker build . --file Dockerfile --platform linux/amd64 --tag issue-metrics:"$(date +%s)" diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index 4874d344..f522353f 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -18,7 +18,7 @@ jobs: statuses: write steps: - name: Checkout Code - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: # Full git history is needed to get a proper # list of changed files within `super-linter` diff --git a/.github/workflows/major-version-updater.yml b/.github/workflows/major-version-updater.yml index 330e4548..fcd89608 100644 --- a/.github/workflows/major-version-updater.yml +++ b/.github/workflows/major-version-updater.yml @@ -15,7 +15,7 @@ jobs: contents: write steps: - name: Checkout Repo - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: version id: version diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 229d93a8..66ae445a 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -22,7 +22,7 @@ jobs: python-version: [3.11, 3.12] steps: - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1b8cf42f..3ef53c27 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,7 +60,7 @@ jobs: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Push Docker Image if: ${{ success() }} uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 13a8a3d7..38c5553e 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -25,7 +25,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: persist-credentials: false @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.24.9 + uses: github/codeql-action/upload-sarif@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.24.9 with: sarif_file: results.sarif From beee08b60de5ff5d59e12cc896d3864b90b3c44e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 18:02:40 +0000 Subject: [PATCH 094/358] chore(deps): bump docker/build-push-action from 5.4.0 to 6.0.0 Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5.4.0 to 6.0.0. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/ca052bb54ab0790a636c9b5f226502c73d547a25...c382f710d39a5bb4e430307530a720f50c2d3318) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1b8cf42f..d7c94265 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,7 +63,7 @@ jobs: - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Push Docker Image if: ${{ success() }} - uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 + uses: docker/build-push-action@c382f710d39a5bb4e430307530a720f50c2d3318 with: context: . file: ./Dockerfile From 35a8242b4de3143abb479f3eae22dc4fd4400fbf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 18:50:58 +0000 Subject: [PATCH 095/358] chore(deps): bump python from `e3ae8cf` to `2fba8e7` Bumps python from `e3ae8cf` to `2fba8e7`. --- updated-dependencies: - dependency-name: python dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 318e6498..4439c588 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.12-slim@sha256:e3ae8cf03c4f0abbfef13a8147478a7cd92798a94fa729a36a185d9106cbae32 +FROM python:3.12-slim@sha256:2fba8e70a87bcc9f6edd20dda0a1d4adb32046d2acbca7361bc61da5a106a914 LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From 0eede17aabb9d97b83ca6bb3b83709c146f04bb6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jun 2024 17:17:05 +0000 Subject: [PATCH 096/358] chore(deps): bump docker/build-push-action in the dependencies group Bumps the dependencies group with 1 update: [docker/build-push-action](https://github.com/docker/build-push-action). Updates `docker/build-push-action` from 6.0.0 to 6.1.0 - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/c382f710d39a5bb4e430307530a720f50c2d3318...31159d49c0d4756269a0940a750801a1ea5d7003) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bae79046..2af712d1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,7 +63,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Push Docker Image if: ${{ success() }} - uses: docker/build-push-action@c382f710d39a5bb4e430307530a720f50c2d3318 + uses: docker/build-push-action@31159d49c0d4756269a0940a750801a1ea5d7003 with: context: . file: ./Dockerfile From 5cf5de88cc65def6c2d3bec2114fdf43faadca09 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jun 2024 18:00:11 +0000 Subject: [PATCH 097/358] chore(deps): bump types-requests from 2.32.0.20240602 to 2.32.0.20240622 Bumps [types-requests](https://github.com/python/typeshed) from 2.32.0.20240602 to 2.32.0.20240622. - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index b5e62471..469c6ad0 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -6,4 +6,4 @@ pylint==3.2.3 pytest==8.2.2 pytest-cov==5.0.0 types-pytz==2024.1.0.20240417 -types-requests==2.32.0.20240602 +types-requests==2.32.0.20240622 From 1affdcf9af77502fec6a6136e04015bc6586bb66 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Mon, 24 Jun 2024 13:55:22 -0700 Subject: [PATCH 098/358] fix: catch error that occurs when ghost usre creates an event. Signed-off-by: Zack Koppert --- time_to_ready_for_review.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/time_to_ready_for_review.py b/time_to_ready_for_review.py index 8c744b1f..f95ffc31 100644 --- a/time_to_ready_for_review.py +++ b/time_to_ready_for_review.py @@ -37,8 +37,14 @@ def get_time_to_ready_for_review( return None events = issue.issue.events(number=50) - for event in events: - if event.event == "ready_for_review": - return event.created_at + try: + for event in events: + if event.event == "ready_for_review": + return event.created_at + except TypeError as e: + print( + f"An error occurred processing review events. Perhaps issue contains a ghost user. {e}" + ) + return None return None From 511a59e1d3debcd503582a414ac29dd5f106df05 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 25 Jun 2024 08:09:56 -0700 Subject: [PATCH 099/358] fix: Catch review comments with no valid user Signed-off-by: Zack Koppert --- time_to_first_response.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/time_to_first_response.py b/time_to_first_response.py index 1f2dc1a1..4de1d2b7 100644 --- a/time_to_first_response.py +++ b/time_to_first_response.py @@ -73,17 +73,22 @@ def measure_time_to_first_response( # so we may also get the first review comment time if pull_request: review_comments = pull_request.reviews(number=50) # type: ignore - for review_comment in review_comments: - if ignore_comment( - issue.issue.user, - review_comment.user, - ignore_users, - review_comment.submitted_at, - ready_for_review_at, - ): - continue - first_review_comment_time = review_comment.submitted_at - break + try: + for review_comment in review_comments: + if ignore_comment( + issue.issue.user, + review_comment.user, + ignore_users, + review_comment.submitted_at, + ready_for_review_at, + ): + continue + first_review_comment_time = review_comment.submitted_at + break + except TypeError as e: + print( + f"An error occurred processing review comments. Perhaps the review contains a ghost user. {e}" + ) # Figure out the earliest response timestamp if first_comment_time and first_review_comment_time: From ccc66bdd212be4dfd4516eb2ad1b71bef2680ca7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:35:51 +0000 Subject: [PATCH 100/358] chore(deps): bump the dependencies group with 2 updates Bumps the dependencies group with 2 updates: [mypy](https://github.com/python/mypy) and [pylint](https://github.com/pylint-dev/pylint). Updates `mypy` from 1.10.0 to 1.10.1 - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.10.0...v1.10.1) Updates `pylint` from 3.2.3 to 3.2.5 - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v3.2.3...v3.2.5) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: pylint dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index 469c6ad0..e89cc98e 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,8 +1,8 @@ black==24.4.2 flake8==7.1.0 -mypy==1.10.0 +mypy==1.10.1 mypy-extensions==1.0.0 -pylint==3.2.3 +pylint==3.2.5 pytest==8.2.2 pytest-cov==5.0.0 types-pytz==2024.1.0.20240417 From f5867b252659975294c867d8fe6f99e2c19ce575 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:42:25 +0000 Subject: [PATCH 101/358] chore(deps): bump python from `2fba8e7` to `da2d7af` Bumps python from `2fba8e7` to `da2d7af`. --- updated-dependencies: - dependency-name: python dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4439c588..59e1c53d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.12-slim@sha256:2fba8e70a87bcc9f6edd20dda0a1d4adb32046d2acbca7361bc61da5a106a914 +FROM python:3.12-slim@sha256:da2d7af143dab7cd5b0d5a5c9545fe14e67fc24c394fcf1cf15e8ea16cbd8637 LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From f3e4f95c67031639519e57f53821a79cdf0d2a51 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:45:58 +0000 Subject: [PATCH 102/358] chore(deps): bump the dependencies group with 3 updates Bumps the dependencies group with 3 updates: [amannn/action-semantic-pull-request](https://github.com/amannn/action-semantic-pull-request), [docker/build-push-action](https://github.com/docker/build-push-action) and [github/codeql-action](https://github.com/github/codeql-action). Updates `amannn/action-semantic-pull-request` from 5.5.2 to 5.5.3 - [Release notes](https://github.com/amannn/action-semantic-pull-request/releases) - [Changelog](https://github.com/amannn/action-semantic-pull-request/blob/main/CHANGELOG.md) - [Commits](https://github.com/amannn/action-semantic-pull-request/compare/cfb60706e18bc85e8aec535e3c577abe8f70378e...0723387faaf9b38adef4775cd42cfd5155ed6017) Updates `docker/build-push-action` from 6.1.0 to 6.2.0 - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/31159d49c0d4756269a0940a750801a1ea5d7003...15560696de535e4014efeff63c48f16952e52dd1) Updates `github/codeql-action` from 3.25.10 to 3.25.11 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/23acc5c183826b7a8a97bce3cecc52db901f8251...b611370bb5703a7efb587f9d136a52ea24c5c38c) --- updated-dependencies: - dependency-name: amannn/action-semantic-pull-request dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/pr-title.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/scorecard.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index 3746ac85..0172fff9 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -17,7 +17,7 @@ jobs: name: Validate PR title runs-on: ubuntu-latest steps: - - uses: amannn/action-semantic-pull-request@cfb60706e18bc85e8aec535e3c577abe8f70378e + - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2af712d1..fae7b678 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,7 +63,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Push Docker Image if: ${{ success() }} - uses: docker/build-push-action@31159d49c0d4756269a0940a750801a1ea5d7003 + uses: docker/build-push-action@15560696de535e4014efeff63c48f16952e52dd1 with: context: . file: ./Dockerfile diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 38c5553e..4ce0add8 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.24.9 + uses: github/codeql-action/upload-sarif@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.24.9 with: sarif_file: results.sarif From afb846bccc1db78e237f48cc2bb79bbe9ebbeacd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2024 17:19:32 +0000 Subject: [PATCH 103/358] chore(deps): bump python from `da2d7af` to `d5f1674` Bumps python from `da2d7af` to `d5f1674`. --- updated-dependencies: - dependency-name: python dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 59e1c53d..7b2e2495 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.12-slim@sha256:da2d7af143dab7cd5b0d5a5c9545fe14e67fc24c394fcf1cf15e8ea16cbd8637 +FROM python:3.12-slim@sha256:d5f16749562233aa4bd26538771d76bf0dfd0a0ea7ea8771985e267451397ae4 LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From 2c7ff5e40bfb5944c918ded0cfa420a8cf414230 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2024 18:00:31 +0000 Subject: [PATCH 104/358] chore(deps): bump the dependencies group with 4 updates Bumps the dependencies group with 4 updates: [super-linter/super-linter](https://github.com/super-linter/super-linter), [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action), [docker/build-push-action](https://github.com/docker/build-push-action) and [actions/upload-artifact](https://github.com/actions/upload-artifact). Updates `super-linter/super-linter` from 6.6.0 to 6.7.0 - [Release notes](https://github.com/super-linter/super-linter/releases) - [Changelog](https://github.com/super-linter/super-linter/blob/main/CHANGELOG.md) - [Commits](https://github.com/super-linter/super-linter/compare/88ea3923a7e1f89dd485d079f6eb5f5e8f937589...3fe03abab2eafb293ace16d4a3b07aeabcb3f1a0) Updates `docker/setup-buildx-action` from 3.3.0 to 3.4.0 - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/d70bba72b1f3fd22344832f00baa16ece964efeb...4fd812986e6c8c2a69e18311145f9371337f27d4) Updates `docker/build-push-action` from 6.2.0 to 6.3.0 - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/15560696de535e4014efeff63c48f16952e52dd1...1a162644f9a7e87d8f4b053101d1d9a712edc18c) Updates `actions/upload-artifact` from 4.3.3 to 4.3.4 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/65462800fd760344b1a7b4382951275a0abb4808...0b2256b8c012f0828dc542b3febcab082c67f72b) --- updated-dependencies: - dependency-name: super-linter/super-linter dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: docker/setup-buildx-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/linter.yaml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index f522353f..10017ffa 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -28,7 +28,7 @@ jobs: python -m pip install --upgrade pip pip install -r requirements.txt -r requirements-test.txt - name: Lint Code Base - uses: super-linter/super-linter@88ea3923a7e1f89dd485d079f6eb5f5e8f937589 + uses: super-linter/super-linter@3fe03abab2eafb293ace16d4a3b07aeabcb3f1a0 env: DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fae7b678..362f9f9f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,7 +53,7 @@ jobs: IMAGE_NAME: github/issue_metrics # different than repo name (underscore instead of dash) steps: - name: Set up Docker Buildx - uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb + uses: docker/setup-buildx-action@4fd812986e6c8c2a69e18311145f9371337f27d4 - name: Log in to the Container registry uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 with: @@ -63,7 +63,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Push Docker Image if: ${{ success() }} - uses: docker/build-push-action@15560696de535e4014efeff63c48f16952e52dd1 + uses: docker/build-push-action@1a162644f9a7e87d8f4b053101d1d9a712edc18c with: context: . file: ./Dockerfile diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 4ce0add8..78ad1cd6 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -36,7 +36,7 @@ jobs: results_format: sarif publish_results: true - name: "Upload artifact" - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 with: name: SARIF file path: results.sarif From 12de356b9d35d8dfd96aa4aac2e18946fb76a15c Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Mon, 8 Jul 2024 16:59:30 -0700 Subject: [PATCH 105/358] fix: natural language --- docs/assign-team-instead-of-individual.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/assign-team-instead-of-individual.md b/docs/assign-team-instead-of-individual.md index 57f0f0d2..11bdf349 100644 --- a/docs/assign-team-instead-of-individual.md +++ b/docs/assign-team-instead-of-individual.md @@ -1,6 +1,6 @@ # Assigning teams instead of individuals -The assignee part of this workflow action comes from [a different GitHub action](https://github.com/peter-evans/create-issue-from-file) and currently GitHub issues don't support assigning groups. +The assignee part of this workflow action comes from [a different GitHub Action](https://github.com/peter-evans/create-issue-from-file) and currently GitHub issues don't support assigning groups. By way of work around, you could use the [GitHub API to retrieve the members of the team](https://docs.github.com/en/rest/teams/members?apiVersion=2022-11-28#list-team-members) and then put them in a comma separated string that you provide as the assignee. From 1c580ab38ca36273bf29fa02a4c08a0642db0a28 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Mon, 8 Jul 2024 17:00:12 -0700 Subject: [PATCH 106/358] fix:natural language --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2db3bd6c..9ab9bfe3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,7 @@ If you then still feel the need to ask a question and need clarification, we rec - Open an [Issue](https://github.com/github/issue-metrics/issues/new). - Provide as much context as you can about what you're running into. -- Provide project and platform versions (nodejs, npm, etc), depending on what seems relevant. +- Provide project and platform versions (Node.js, npm, etc), depending on what seems relevant. We will then take care of the issue as soon as possible. From d54c708f4a635ae56ffa843544ff88944389fc49 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:29:35 +0000 Subject: [PATCH 107/358] chore(deps): bump types-requests from 2.32.0.20240622 to 2.32.0.20240712 Bumps [types-requests](https://github.com/python/typeshed) from 2.32.0.20240622 to 2.32.0.20240712. - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index e89cc98e..b2df3126 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -6,4 +6,4 @@ pylint==3.2.5 pytest==8.2.2 pytest-cov==5.0.0 types-pytz==2024.1.0.20240417 -types-requests==2.32.0.20240622 +types-requests==2.32.0.20240712 From a00720b7abec4af2984fd4764b933b64bc7d34c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:45:25 +0000 Subject: [PATCH 108/358] chore(deps): bump python from `d5f1674` to `f11725a` Bumps python from `d5f1674` to `f11725a`. --- updated-dependencies: - dependency-name: python dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 7b2e2495..fb54b1bf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.12-slim@sha256:d5f16749562233aa4bd26538771d76bf0dfd0a0ea7ea8771985e267451397ae4 +FROM python:3.12-slim@sha256:f11725aba18c19664a408902103365eaf8013823ffc56270f921d1dc78a198cb LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From 0cc1395c2250b5952295a7b5d25e6df2f0b969db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jul 2024 18:01:37 +0000 Subject: [PATCH 109/358] chore(deps): bump the dependencies group with 3 updates Bumps the dependencies group with 3 updates: [actions/setup-python](https://github.com/actions/setup-python), [docker/build-push-action](https://github.com/docker/build-push-action) and [github/codeql-action](https://github.com/github/codeql-action). Updates `actions/setup-python` from 5.1.0 to 5.1.1 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/82c7e631bb3cdc910f68e0081d67478d79c6982d...39cd14951b08e74b54015e9e001cdefcf80e669f) Updates `docker/build-push-action` from 6.3.0 to 6.4.0 - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/1a162644f9a7e87d8f4b053101d1d9a712edc18c...a254f8ca60a858f3136a2f1f23a60969f2c402dd) Updates `github/codeql-action` from 3.25.11 to 3.25.12 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/b611370bb5703a7efb587f9d136a52ea24c5c38c...4fa2a7953630fd2f3fb380f21be14ede0169dd4f) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/python-package.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/scorecard.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 66ae445a..074af784 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -24,7 +24,7 @@ jobs: steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 362f9f9f..2efc5985 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,7 +63,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Push Docker Image if: ${{ success() }} - uses: docker/build-push-action@1a162644f9a7e87d8f4b053101d1d9a712edc18c + uses: docker/build-push-action@a254f8ca60a858f3136a2f1f23a60969f2c402dd with: context: . file: ./Dockerfile diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 78ad1cd6..298d8b60 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.24.9 + uses: github/codeql-action/upload-sarif@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.24.9 with: sarif_file: results.sarif From 80f0a05695f545183d8cc1fa33a00cf22e2f34ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jul 2024 17:10:39 +0000 Subject: [PATCH 110/358] chore(deps): bump the dependencies group with 4 updates Bumps the dependencies group with 4 updates: [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action), [docker/login-action](https://github.com/docker/login-action), [docker/build-push-action](https://github.com/docker/build-push-action) and [github/codeql-action](https://github.com/github/codeql-action). Updates `docker/setup-buildx-action` from 3.4.0 to 3.5.0 - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/4fd812986e6c8c2a69e18311145f9371337f27d4...aa33708b10e362ff993539393ff100fa93ed6a27) Updates `docker/login-action` from 3.2.0 to 3.3.0 - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/0d4c9c5ea7693da7b068278f7b52bda2a190a446...9780b0c442fbb1117ed29e0efdff1e18412f7567) Updates `docker/build-push-action` from 6.4.0 to 6.5.0 - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/a254f8ca60a858f3136a2f1f23a60969f2c402dd...5176d81f87c23d6fc96624dfdbcd9f3830bbe445) Updates `github/codeql-action` from 3.25.12 to 3.25.13 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/4fa2a7953630fd2f3fb380f21be14ede0169dd4f...2d790406f505036ef40ecba973cc774a50395aac) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: docker/login-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 6 +++--- .github/workflows/scorecard.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2efc5985..adb099d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,9 +53,9 @@ jobs: IMAGE_NAME: github/issue_metrics # different than repo name (underscore instead of dash) steps: - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4fd812986e6c8c2a69e18311145f9371337f27d4 + uses: docker/setup-buildx-action@aa33708b10e362ff993539393ff100fa93ed6a27 - name: Log in to the Container registry - uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -63,7 +63,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Push Docker Image if: ${{ success() }} - uses: docker/build-push-action@a254f8ca60a858f3136a2f1f23a60969f2c402dd + uses: docker/build-push-action@5176d81f87c23d6fc96624dfdbcd9f3830bbe445 with: context: . file: ./Dockerfile diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 298d8b60..2301e69c 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.24.9 + uses: github/codeql-action/upload-sarif@2d790406f505036ef40ecba973cc774a50395aac # v3.24.9 with: sarif_file: results.sarif From 113d84d931dd8ac2a65a9bcfbecf433cf8957162 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jul 2024 17:57:02 +0000 Subject: [PATCH 111/358] chore(deps): bump the dependencies group with 4 updates Bumps the dependencies group with 4 updates: [numpy](https://github.com/numpy/numpy), [mypy](https://github.com/python/mypy), [pylint](https://github.com/pylint-dev/pylint) and [pytest](https://github.com/pytest-dev/pytest). Updates `numpy` from 2.0.0 to 2.0.1 - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.0.0...v2.0.1) Updates `mypy` from 1.10.1 to 1.11.0 - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.10.1...v1.11) Updates `pylint` from 3.2.5 to 3.2.6 - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v3.2.5...v3.2.6) Updates `pytest` from 8.2.2 to 8.3.1 - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.2.2...8.3.1) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: pylint dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 6 +++--- requirements.txt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index b2df3126..e0f068f3 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,9 +1,9 @@ black==24.4.2 flake8==7.1.0 -mypy==1.10.1 +mypy==1.11.0 mypy-extensions==1.0.0 -pylint==3.2.5 -pytest==8.2.2 +pylint==3.2.6 +pytest==8.3.1 pytest-cov==5.0.0 types-pytz==2024.1.0.20240417 types-requests==2.32.0.20240712 diff --git a/requirements.txt b/requirements.txt index f6f4dbcf..13c14843 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ github3.py==4.0.1 -numpy==2.0.0 +numpy==2.0.1 python-dotenv==1.0.1 pytz==2024.1 requests==2.32.3 From 7241820ca2a2b8db521ade8a6c6f7684de7781ff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 17:20:15 +0000 Subject: [PATCH 112/358] chore(deps): bump pytest from 8.3.1 to 8.3.2 in the dependencies group Bumps the dependencies group with 1 update: [pytest](https://github.com/pytest-dev/pytest). Updates `pytest` from 8.3.1 to 8.3.2 - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.3.1...8.3.2) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index e0f068f3..64c7f4e7 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -3,7 +3,7 @@ flake8==7.1.0 mypy==1.11.0 mypy-extensions==1.0.0 pylint==3.2.6 -pytest==8.3.1 +pytest==8.3.2 pytest-cov==5.0.0 types-pytz==2024.1.0.20240417 types-requests==2.32.0.20240712 From c62b589aba0cdbfce0d70de30696d0b83dbe315c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 17:31:00 +0000 Subject: [PATCH 113/358] chore(deps): bump python from `f11725a` to `740d94a` Bumps python from `f11725a` to `740d94a`. --- updated-dependencies: - dependency-name: python dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index fb54b1bf..665c480b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.12-slim@sha256:f11725aba18c19664a408902103365eaf8013823ffc56270f921d1dc78a198cb +FROM python:3.12-slim@sha256:740d94a19218c8dd584b92f804b1158f85b0d241e5215ea26ed2dcade2b9d138 LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From 961ea47a20ea5c5f73a65c18bd3b64c740e40e47 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 17:47:50 +0000 Subject: [PATCH 114/358] chore(deps): bump the dependencies group with 3 updates Bumps the dependencies group with 3 updates: [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action), [ossf/scorecard-action](https://github.com/ossf/scorecard-action) and [github/codeql-action](https://github.com/github/codeql-action). Updates `docker/setup-buildx-action` from 3.5.0 to 3.6.1 - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/aa33708b10e362ff993539393ff100fa93ed6a27...988b5a0280414f521da01fcc63a27aeeb4b104db) Updates `ossf/scorecard-action` from 2.3.3 to 2.4.0 - [Release notes](https://github.com/ossf/scorecard-action/releases) - [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md) - [Commits](https://github.com/ossf/scorecard-action/compare/dc50aa9510b46c811795eb24b2f1ba02a914e534...62b2cac7ed8198b15735ed49ab1e5cf35480ba46) Updates `github/codeql-action` from 3.25.13 to 3.25.15 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/2d790406f505036ef40ecba973cc774a50395aac...afb54ba388a7dca6ecae48f608c4ff05ff4cc77a) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: ossf/scorecard-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- .github/workflows/scorecard.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index adb099d5..f9db2d21 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,7 +53,7 @@ jobs: IMAGE_NAME: github/issue_metrics # different than repo name (underscore instead of dash) steps: - name: Set up Docker Buildx - uses: docker/setup-buildx-action@aa33708b10e362ff993539393ff100fa93ed6a27 + uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db - name: Log in to the Container registry uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 with: diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 2301e69c..15dde336 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.3 + uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 with: results_file: results.sarif results_format: sarif @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@2d790406f505036ef40ecba973cc774a50395aac # v3.24.9 + uses: github/codeql-action/upload-sarif@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.24.9 with: sarif_file: results.sarif From 9ae097d24eb6927a441a53a1ef9527d449c7c7d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:09:27 +0000 Subject: [PATCH 115/358] chore(deps): bump the dependencies group with 3 updates Bumps the dependencies group with 3 updates: [black](https://github.com/psf/black), [flake8](https://github.com/pycqa/flake8) and [mypy](https://github.com/python/mypy). Updates `black` from 24.4.2 to 24.8.0 - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/compare/24.4.2...24.8.0) Updates `flake8` from 7.1.0 to 7.1.1 - [Commits](https://github.com/pycqa/flake8/compare/7.1.0...7.1.1) Updates `mypy` from 1.11.0 to 1.11.1 - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.11...v1.11.1) --- updated-dependencies: - dependency-name: black dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: flake8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index 64c7f4e7..9bcd7a2d 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,6 +1,6 @@ -black==24.4.2 -flake8==7.1.0 -mypy==1.11.0 +black==24.8.0 +flake8==7.1.1 +mypy==1.11.1 mypy-extensions==1.0.0 pylint==3.2.6 pytest==8.3.2 From 858e1fc61e0e5b364ba596be471ab0067cb16dd1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:14:19 +0000 Subject: [PATCH 116/358] chore(deps): bump the dependencies group with 2 updates Bumps the dependencies group with 2 updates: [super-linter/super-linter](https://github.com/super-linter/super-linter) and [actions/upload-artifact](https://github.com/actions/upload-artifact). Updates `super-linter/super-linter` from 6.7.0 to 6.8.0 - [Release notes](https://github.com/super-linter/super-linter/releases) - [Changelog](https://github.com/super-linter/super-linter/blob/main/CHANGELOG.md) - [Commits](https://github.com/super-linter/super-linter/compare/3fe03abab2eafb293ace16d4a3b07aeabcb3f1a0...b4515bd4ad9d0aa4681960e053916ab991bdbe96) Updates `actions/upload-artifact` from 4.3.4 to 4.3.5 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/0b2256b8c012f0828dc542b3febcab082c67f72b...89ef406dd8d7e03cfd12d9e0a4a378f454709029) --- updated-dependencies: - dependency-name: super-linter/super-linter dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/linter.yaml | 2 +- .github/workflows/scorecard.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index 10017ffa..1c40e977 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -28,7 +28,7 @@ jobs: python -m pip install --upgrade pip pip install -r requirements.txt -r requirements-test.txt - name: Lint Code Base - uses: super-linter/super-linter@3fe03abab2eafb293ace16d4a3b07aeabcb3f1a0 + uses: super-linter/super-linter@b4515bd4ad9d0aa4681960e053916ab991bdbe96 env: DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 15dde336..cd690cba 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -36,7 +36,7 @@ jobs: results_format: sarif publish_results: true - name: "Upload artifact" - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5 with: name: SARIF file path: results.sarif From a1783ce5a4f89c9fa027ea68055071b4be561be6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:40:35 +0000 Subject: [PATCH 117/358] chore(deps): bump python from `740d94a` to `a3e58f9` Bumps python from `740d94a` to `a3e58f9`. --- updated-dependencies: - dependency-name: python dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 665c480b..b46eb5a0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.12-slim@sha256:740d94a19218c8dd584b92f804b1158f85b0d241e5215ea26ed2dcade2b9d138 +FROM python:3.12-slim@sha256:a3e58f9399353be051735f09be0316bfdeab571a5c6a24fd78b92df85bcb2d85 LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From 3f984a0bd056b2e1ffc1d5b78c9c9fabd96410f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 17:17:35 +0000 Subject: [PATCH 118/358] chore(deps): bump python from `a3e58f9` to `59c7332` Bumps python from `a3e58f9` to `59c7332`. --- updated-dependencies: - dependency-name: python dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index b46eb5a0..9e4294b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.12-slim@sha256:a3e58f9399353be051735f09be0316bfdeab571a5c6a24fd78b92df85bcb2d85 +FROM python:3.12-slim@sha256:59c7332a4a24373861c4a5f0eec2c92b87e3efeb8ddef011744ef9a751b1d11c LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From 2df818392ea1830ab9a8331625e1c5633d1d4963 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 17:21:59 +0000 Subject: [PATCH 119/358] chore(deps): bump numpy from 2.0.1 to 2.1.0 in the dependencies group Bumps the dependencies group with 1 update: [numpy](https://github.com/numpy/numpy). Updates `numpy` from 2.0.1 to 2.1.0 - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.0.1...v2.1.0) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 13c14843..4690c546 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ github3.py==4.0.1 -numpy==2.0.1 +numpy==2.1.0 python-dotenv==1.0.1 pytz==2024.1 requests==2.32.3 From 90579a03ce5f32c0ca5fbbf26d6820ff019028b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 17:34:10 +0000 Subject: [PATCH 120/358] chore(deps): bump the dependencies group with 3 updates Bumps the dependencies group with 3 updates: [docker/build-push-action](https://github.com/docker/build-push-action), [actions/upload-artifact](https://github.com/actions/upload-artifact) and [github/codeql-action](https://github.com/github/codeql-action). Updates `docker/build-push-action` from 6.5.0 to 6.7.0 - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/5176d81f87c23d6fc96624dfdbcd9f3830bbe445...5cd11c3a4ced054e52742c5fd54dca954e0edd85) Updates `actions/upload-artifact` from 4.3.5 to 4.3.6 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/89ef406dd8d7e03cfd12d9e0a4a378f454709029...834a144ee995460fba8ed112a2fc961b36a5ec5a) Updates `github/codeql-action` from 3.25.15 to 3.26.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/afb54ba388a7dca6ecae48f608c4ff05ff4cc77a...883d8588e56d1753a8a58c1c86e88976f0c23449) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- .github/workflows/scorecard.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f9db2d21..62ee736d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,7 +63,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Push Docker Image if: ${{ success() }} - uses: docker/build-push-action@5176d81f87c23d6fc96624dfdbcd9f3830bbe445 + uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 with: context: . file: ./Dockerfile diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index cd690cba..6c056f1b 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -36,12 +36,12 @@ jobs: results_format: sarif publish_results: true - name: "Upload artifact" - uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: SARIF file path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.24.9 + uses: github/codeql-action/upload-sarif@883d8588e56d1753a8a58c1c86e88976f0c23449 # v3.24.9 with: sarif_file: results.sarif From 4f48c5736623cc97373da4cfb06a89bbdfddc4e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 17:34:14 +0000 Subject: [PATCH 121/358] chore(deps): bump super-linter/super-linter from 6.8.0 to 7.0.0 Bumps [super-linter/super-linter](https://github.com/super-linter/super-linter) from 6.8.0 to 7.0.0. - [Release notes](https://github.com/super-linter/super-linter/releases) - [Changelog](https://github.com/super-linter/super-linter/blob/main/CHANGELOG.md) - [Commits](https://github.com/super-linter/super-linter/compare/b4515bd4ad9d0aa4681960e053916ab991bdbe96...02a1172d274f021e4c70f66e23f1085eadd1064b) --- updated-dependencies: - dependency-name: super-linter/super-linter dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/linter.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index 1c40e977..8086db0c 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -28,7 +28,7 @@ jobs: python -m pip install --upgrade pip pip install -r requirements.txt -r requirements-test.txt - name: Lint Code Base - uses: super-linter/super-linter@b4515bd4ad9d0aa4681960e053916ab991bdbe96 + uses: super-linter/super-linter@02a1172d274f021e4c70f66e23f1085eadd1064b env: DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 5c75c3376f4d0733c4182d782294346b7aa95cf6 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Mon, 19 Aug 2024 12:38:28 -0500 Subject: [PATCH 122/358] fix: run prettier on all files suggested by new version of super-linter Signed-off-by: jmeridth --- .devcontainer/devcontainer.json | 18 +-- .github/pull_request_template.md | 4 +- .github/release-drafter.yml | 80 +++++------ .github/workflows/auto-labeler.yml | 2 +- .github/workflows/contributor_report.yaml | 61 +++++---- .github/workflows/docker-image.yml | 10 +- .github/workflows/linter.yaml | 2 +- .github/workflows/major-version-updater.yml | 2 +- .github/workflows/pr-title.yml | 2 +- .github/workflows/python-package.yml | 35 +++-- .github/workflows/release.yml | 4 +- .github/workflows/scorecard.yml | 4 +- .github/workflows/stale.yaml | 8 +- .vscode/settings.json | 18 ++- CONTRIBUTING.md | 9 +- README.md | 124 +++++++++--------- action.yml | 14 +- docs/dealing-with-large-issue-metrics.md | 1 + ...mple-using-json-instead-markdown-output.md | 19 ++- docs/example-workflows.md | 82 ++++++------ docs/measure-time.md | 113 ++++++++-------- 21 files changed, 304 insertions(+), 308 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 0d380a23..63126916 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,13 +1,13 @@ { - "name": "Existing Dockerfile", - "build": { - "context": "..", - "dockerfile": "../Dockerfile" - }, + "name": "Existing Dockerfile", + "build": { + "context": "..", + "dockerfile": "../Dockerfile" + }, - "features": { - "ghcr.io/devcontainers/features/common-utils:2": {} - }, + "features": { + "ghcr.io/devcontainers/features/common-utils:2": {} + }, - "remoteUser": "devcontainer" + "remoteUser": "devcontainer" } diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 57f06b32..e0a63f42 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,5 +1,6 @@ # Pull Request - ## Proposed Changes + ## Readiness Checklist diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 33db4984..2ba0ceee 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,6 +1,6 @@ --- -name-template: 'v$RESOLVED_VERSION' -tag-template: 'v$RESOLVED_VERSION' +name-template: "v$RESOLVED_VERSION" +tag-template: "v$RESOLVED_VERSION" template: | # Changelog $CHANGES @@ -8,61 +8,61 @@ template: | See details of [all code changes](https://github.com/github/issue-metrics/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION) since previous release categories: - - title: '🚀 Features' + - title: "🚀 Features" labels: - - 'feature' - - 'enhancement' - - title: '🐛 Bug Fixes' + - "feature" + - "enhancement" + - title: "🐛 Bug Fixes" labels: - - 'fix' - - 'bugfix' - - 'bug' - - title: '🧰 Maintenance' + - "fix" + - "bugfix" + - "bug" + - title: "🧰 Maintenance" labels: - - 'infrastructure' - - 'automation' - - 'documentation' - - 'dependencies' - - 'maintenance' - - 'revert' - - title: '🏎 Performance' - label: 'performance' -change-template: '- $TITLE @$AUTHOR (#$NUMBER)' + - "infrastructure" + - "automation" + - "documentation" + - "dependencies" + - "maintenance" + - "revert" + - title: "🏎 Performance" + label: "performance" +change-template: "- $TITLE @$AUTHOR (#$NUMBER)" version-resolver: major: labels: - - 'breaking' + - "breaking" minor: labels: - - 'enhancement' - - 'fix' + - "enhancement" + - "fix" patch: labels: - - 'documentation' - - 'maintenance' + - "documentation" + - "maintenance" default: patch autolabeler: - - label: 'automation' + - label: "automation" title: - - '/^(build|ci|perf|refactor|test).*/i' - - label: 'enhancement' + - "/^(build|ci|perf|refactor|test).*/i" + - label: "enhancement" title: - - '/^(style).*/i' - - label: 'documentation' + - "/^(style).*/i" + - label: "documentation" title: - - '/^(docs).*/i' - - label: 'feature' + - "/^(docs).*/i" + - label: "feature" title: - - '/^(feat).*/i' - - label: 'fix' + - "/^(feat).*/i" + - label: "fix" title: - - '/^(fix).*/i' - - label: 'infrastructure' + - "/^(fix).*/i" + - label: "infrastructure" title: - - '/^(infrastructure).*/i' - - label: 'maintenance' + - "/^(infrastructure).*/i" + - label: "maintenance" title: - - '/^(chore|maintenance).*/i' - - label: 'revert' + - "/^(chore|maintenance).*/i" + - label: "revert" title: - - '/^(revert).*/i' + - "/^(revert).*/i" diff --git a/.github/workflows/auto-labeler.yml b/.github/workflows/auto-labeler.yml index 7ee19283..9fe305e1 100644 --- a/.github/workflows/auto-labeler.yml +++ b/.github/workflows/auto-labeler.yml @@ -4,7 +4,7 @@ name: Auto Labeler on: # pull_request_target event is required for autolabeler to support all PRs including forks pull_request_target: - types: [ opened, reopened, edited, synchronize ] + types: [opened, reopened, edited, synchronize] permissions: contents: read diff --git a/.github/workflows/contributor_report.yaml b/.github/workflows/contributor_report.yaml index 6ada8406..885d47be 100644 --- a/.github/workflows/contributor_report.yaml +++ b/.github/workflows/contributor_report.yaml @@ -2,7 +2,7 @@ name: Monthly contributor report on: workflow_dispatch: schedule: - - cron: '3 2 1 * *' + - cron: "3 2 1 * *" permissions: issues: write @@ -13,33 +13,32 @@ jobs: runs-on: ubuntu-latest steps: - - - name: Get dates for last month - shell: bash - run: | - # Calculate the first day of the previous month - start_date=$(date -d "last month" +%Y-%m-01) - - # Calculate the last day of the previous month - end_date=$(date -d "$start_date +1 month -1 day" +%Y-%m-%d) - - #Set an environment variable with the date range - echo "START_DATE=$start_date" >> "$GITHUB_ENV" - echo "END_DATE=$end_date" >> "$GITHUB_ENV" - - - name: Run contributor action - uses: github/contributors@135b0430e856ade27175cbd1d4e1e11b0dd8ef95 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - START_DATE: ${{ env.START_DATE }} - END_DATE: ${{ env.END_DATE }} - REPOSITORY: github/issue-metrics - SPONSOR_INFO: "true" - - - name: Create issue - uses: peter-evans/create-issue-from-file@24452a72d85239eacf1468b0f1982a9f3fec4c94 - with: - title: Monthly contributor report - token: ${{ secrets.GITHUB_TOKEN }} - content-filepath: ./contributors.md - assignees: zkoppert + - name: Get dates for last month + shell: bash + run: | + # Calculate the first day of the previous month + start_date=$(date -d "last month" +%Y-%m-01) + + # Calculate the last day of the previous month + end_date=$(date -d "$start_date +1 month -1 day" +%Y-%m-%d) + + #Set an environment variable with the date range + echo "START_DATE=$start_date" >> "$GITHUB_ENV" + echo "END_DATE=$end_date" >> "$GITHUB_ENV" + + - name: Run contributor action + uses: github/contributors@135b0430e856ade27175cbd1d4e1e11b0dd8ef95 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + START_DATE: ${{ env.START_DATE }} + END_DATE: ${{ env.END_DATE }} + REPOSITORY: github/issue-metrics + SPONSOR_INFO: "true" + + - name: Create issue + uses: peter-evans/create-issue-from-file@24452a72d85239eacf1468b0f1982a9f3fec4c94 + with: + title: Monthly contributor report + token: ${{ secrets.GITHUB_TOKEN }} + content-filepath: ./contributors.md + assignees: zkoppert diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 6da5e769..0af83305 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -3,9 +3,9 @@ name: Docker Image CI on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] permissions: contents: read @@ -14,6 +14,6 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - name: Build the Docker image - run: docker build . --file Dockerfile --platform linux/amd64 --tag issue-metrics:"$(date +%s)" + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: Build the Docker image + run: docker build . --file Dockerfile --platform linux/amd64 --tag issue-metrics:"$(date +%s)" diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index 8086db0c..9a918d6e 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -3,7 +3,7 @@ name: Lint Code Base on: pull_request: - branches: [ main ] + branches: [main] permissions: contents: read diff --git a/.github/workflows/major-version-updater.yml b/.github/workflows/major-version-updater.yml index fcd89608..dafb6f27 100644 --- a/.github/workflows/major-version-updater.yml +++ b/.github/workflows/major-version-updater.yml @@ -3,7 +3,7 @@ name: Major Version Updater # Whenever a new release is made, push a major version tag on: release: - types: [ published ] + types: [published] permissions: contents: write diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index 0172fff9..08a66253 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -4,7 +4,7 @@ name: "Lint PR Title" on: pull_request_target: - types: [ opened, reopened, edited, synchronize ] + types: [opened, reopened, edited, synchronize] permissions: contents: read diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 074af784..e8058320 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -6,34 +6,33 @@ name: Python package on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] permissions: contents: read jobs: build: - runs-on: ubuntu-latest strategy: matrix: python-version: [3.11, 3.12] steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt -r requirements-test.txt - - name: Lint with flake8 and pylint - run: | - make lint - - name: Test with pytest - run: | - make test + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt -r requirements-test.txt + - name: Lint with flake8 and pylint + run: | + make lint + - name: Test with pytest + run: | + make test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 62ee736d..ecac2b9a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,8 +4,8 @@ name: Release on: workflow_dispatch: pull_request_target: - types: [ closed ] - branches: [ main ] + types: [closed] + branches: [main] permissions: contents: read diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 6c056f1b..7312060f 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -9,9 +9,9 @@ on: # To guarantee Maintained check is occasionally updated. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained schedule: - - cron: '29 11 * * 6' + - cron: "29 11 * * 6" push: - branches: [ main ] + branches: [main] permissions: read-all diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 5a0bc415..94485211 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -1,7 +1,7 @@ -name: 'Close stale issues' +name: "Close stale issues" on: schedule: - - cron: '30 1 * * *' + - cron: "30 1 * * *" permissions: issues: write @@ -13,8 +13,8 @@ jobs: steps: - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e with: - stale-issue-message: 'This issue is stale because it has been open 21 days with no activity. Remove stale label or comment or this will be closed in 14 days.' - close-issue-message: 'This issue was closed because it has been stalled for 35 days with no activity.' + stale-issue-message: "This issue is stale because it has been open 21 days with no activity. Remove stale label or comment or this will be closed in 14 days." + close-issue-message: "This issue was closed because it has been stalled for 35 days with no activity." days-before-stale: 21 days-before-close: 14 days-before-pr-close: -1 diff --git a/.vscode/settings.json b/.vscode/settings.json index 60561a45..705ed9b5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,9 @@ { - "python.testing.pytestArgs": [ - "." - ], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true, - "[python]": { - "editor.defaultFormatter": "ms-python.black-formatter" - }, - "python.formatting.provider": "none" -} \ No newline at end of file + "python.testing.pytestArgs": ["."], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter" + }, + "python.formatting.provider": "none" +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9ab9bfe3..3299ec78 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,6 @@ + # Contributing to issue-metrics First off, thanks for taking the time to contribute! :heart: @@ -7,6 +8,7 @@ First off, thanks for taking the time to contribute! :heart: All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us project owners and smooth out the experience for all involved. The team looks forward to your contributions. :tada: + ## Table of Contents - [I Have a Question](#i-have-a-question) @@ -30,11 +32,13 @@ We will then take care of the issue as soon as possible. ## I Want To Contribute > ### Legal Notice +> > When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license. ## Reporting Bugs + ### Before Submitting a Bug Report A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible. @@ -50,6 +54,7 @@ A good bug report shouldn't leave others needing to chase you up for more inform - Can you reliably reproduce the issue? And can you also reproduce it with older versions? + ### How Do I Submit a Good Bug Report? Please submit a bug report using our [GitHub Issues template](https://github.com/github/issue-metrics/issues/new?template=bug_report.yml). @@ -59,6 +64,7 @@ Please submit a bug report using our [GitHub Issues template](https://github.com This section guides you through submitting an enhancement suggestion for issue-metrics, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions. + ### Before Submitting an Enhancement - Make sure that you are using the latest version. @@ -67,6 +73,7 @@ This section guides you through submitting an enhancement suggestion for issue-m - Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature or to develop the feature yourself and contribute it to the project. + ### How Do I Submit a Good Enhancement Suggestion? Please submit an enhancement suggestion using our [GitHub Issues template](https://github.com/github/issue-metrics/issues/new?template=feature_request.yml). @@ -79,4 +86,4 @@ We are using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0. Releases are automated if a pull request is labelled with our [SemVer related labels](.github/release-drafter.yml) or with the `vuln` or `release` labels. -You can also manually initiate a release you can do so through the GitHub Actions UI. If you have permissions to do so, you can navigate to the [Actions tab](https://github.com/github/issue-metrics/actions/workflows/release.yml) and select the `Run workflow` button. This will allow you to select the branch to release from and the version to release. +You can also manually initiate a release you can do so through the GitHub Actions UI. If you have permissions to do so, you can navigate to the [Actions tab](https://github.com/github/issue-metrics/actions/workflows/release.yml) and select the `Run workflow` button. This will allow you to select the branch to release from and the version to release. diff --git a/README.md b/README.md index 241fc774..a22b398c 100644 --- a/README.md +++ b/README.md @@ -13,16 +13,16 @@ Feel free to inquire about its usage by creating an issue in this repository. ## Available Metrics -| Metric | Description | -|--------|-------------| -|Time to First Response | The duration from creation to the initial comment or review.*| -|Time to Close | The period from creation to closure.*| -|Time to Answer (Discussions Only) | The time from creation to an answer.| -|Time in Label | The duration from label application to removal, requires `LABELS_TO_MEASURE` env variable.| +| Metric | Description | +| --------------------------------- | ------------------------------------------------------------------------------------------ | +| Time to First Response | The duration from creation to the initial comment or review.\* | +| Time to Close | The period from creation to closure.\* | +| Time to Answer (Discussions Only) | The time from creation to an answer. | +| Time in Label | The duration from label application to removal, requires `LABELS_TO_MEASURE` env variable. | -*For pull requests, these metrics exclude the time the PR was in draft mode. +\*For pull requests, these metrics exclude the time the PR was in draft mode. -*For issues and pull requests, comments by issue/pull request author's and comments by bots are excluded. +\*For issues and pull requests, comments by issue/pull request author's and comments by bots are excluded. To find syntax for search queries, check out the documentation on [searching issues and pull requests](https://docs.github.com/en/issues/tracking-your-work-with-issues/filtering-and-searching-issues-and-pull-requests) or [searching discussions](https://docs.github.com/en/search-github/searching-on-github/searching-discussions). @@ -45,7 +45,7 @@ name: Monthly issue metrics on: workflow_dispatch: schedule: - - cron: '3 2 1 * *' + - cron: "3 2 1 * *" permissions: contents: read @@ -58,31 +58,31 @@ jobs: issues: write pull-requests: read steps: - - name: Get dates for last month - shell: bash - run: | - # Calculate the first day of the previous month - first_day=$(date -d "last month" +%Y-%m-01) - - # Calculate the last day of the previous month - last_day=$(date -d "$first_day +1 month -1 day" +%Y-%m-%d) - - #Set an environment variable with the date range - echo "$first_day..$last_day" - echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV" - - - name: Run issue-metrics tool - uses: github/issue-metrics@v3 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SEARCH_QUERY: 'repo:owner/repo is:issue created:${{ env.last_month }} -reason:"not planned"' - - - name: Create issue - uses: peter-evans/create-issue-from-file@v5 - with: - title: Monthly issue metrics report - token: ${{ secrets.GITHUB_TOKEN }} - content-filepath: ./issue_metrics.md + - name: Get dates for last month + shell: bash + run: | + # Calculate the first day of the previous month + first_day=$(date -d "last month" +%Y-%m-01) + + # Calculate the last day of the previous month + last_day=$(date -d "$first_day +1 month -1 day" +%Y-%m-%d) + + #Set an environment variable with the date range + echo "$first_day..$last_day" + echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV" + + - name: Run issue-metrics tool + uses: github/issue-metrics@v3 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SEARCH_QUERY: 'repo:owner/repo is:issue created:${{ env.last_month }} -reason:"not planned"' + + - name: Create issue + uses: peter-evans/create-issue-from-file@v5 + with: + title: Monthly issue metrics report + token: ${{ secrets.GITHUB_TOKEN }} + content-filepath: ./issue_metrics.md ``` ## Example use cases @@ -107,11 +107,11 @@ All feedback regarding our GitHub Actions, as a whole, should be communicated th 3. Copy that example into your repository (from step 1) and into the proper directory for GitHub Actions: `.github/workflows/` directory with the file extension `.yml` (ie. `.github/workflows/issue-metrics.yml`) 4. Edit the values (`SEARCH_QUERY`, `assignees`) from the sample workflow with your information. See the [SEARCH_QUERY](./docs/search-query.md) section for more information on how to configure the search query. 5. If you are running metrics on a repository other than the one where the workflow file is going to be, then update the value of `GH_TOKEN`. - - Do this by creating a [GitHub API token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic) with permissions to read the repository and write issues. - - Then take the value of the API token you just created, and [create a repository secret](https://docs.github.com/en/actions/security-guides/encrypted-secrets) where the name of the secret is `GH_TOKEN` and the value of the secret the API token. - - Then finally update the workflow file to use that repository secret by changing `GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}` to `GH_TOKEN: ${{ secrets.GH_TOKEN }}`. The name of the secret can really be anything. It just needs to match between when you create the secret name and when you refer to it in the workflow file. + - Do this by creating a [GitHub API token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic) with permissions to read the repository and write issues. + - Then take the value of the API token you just created, and [create a repository secret](https://docs.github.com/en/actions/security-guides/encrypted-secrets) where the name of the secret is `GH_TOKEN` and the value of the secret the API token. + - Then finally update the workflow file to use that repository secret by changing `GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}` to `GH_TOKEN: ${{ secrets.GH_TOKEN }}`. The name of the secret can really be anything. It just needs to match between when you create the secret name and when you refer to it in the workflow file. 6. If you want the resulting issue with the metrics in it to appear in a different repository other than the one the workflow file runs in, update the line `token: ${{ secrets.GITHUB_TOKEN }}` with your own GitHub API token stored as a repository secret. - - This process is the same as described in the step above. More info on creating secrets can be found [here](https://docs.github.com/en/actions/security-guides/encrypted-secrets). + - This process is the same as described in the step above. More info on creating secrets can be found [here](https://docs.github.com/en/actions/security-guides/encrypted-secrets). 7. Commit the workflow file to the default branch (often `master` or `main`) 8. Wait for the action to trigger based on the `schedule` entry or manually trigger the workflow as shown in the [documentation](https://docs.github.com/en/actions/using-workflows/manually-running-a-workflow). @@ -125,36 +125,36 @@ This action can be configured to authenticate with GitHub App Installation or Pe ##### GitHub App Installation -| field | required | default | description | -|-------------------------------|----------|---------|-------------| -| `GH_APP_ID` | True | `""` | GitHub Application ID. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. | -| `GH_APP_INSTALLATION_ID` | True | `""` | GitHub Application Installation ID. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. | -| `GH_APP_PRIVATE_KEY` | True | `""` | GitHub Application Private Key. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. | +| field | required | default | description | +| ------------------------ | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `GH_APP_ID` | True | `""` | GitHub Application ID. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. | +| `GH_APP_INSTALLATION_ID` | True | `""` | GitHub Application Installation ID. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. | +| `GH_APP_PRIVATE_KEY` | True | `""` | GitHub Application Private Key. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. | ##### Personal Access Token (PAT) -| field | required | default | description | -|-------------------------------|----------|---------|-------------| -| `GH_TOKEN` | True | `""` | The GitHub Token used to scan the repository. Must have read access to all repository you are interested in scanning. | +| field | required | default | description | +| ---------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------- | +| `GH_TOKEN` | True | `""` | The GitHub Token used to scan the repository. Must have read access to all repository you are interested in scanning. | #### Other Configuration Options -| field | required | default | description | -|-------------------------------|----------|---------|-------------| -| `GH_ENTERPRISE_URL` | False | `""` | URL of GitHub Enterprise instance to use for auth instead of github.com | -| `HIDE_AUTHOR` | False | False | If set to `true`, the author will not be displayed in the generated Markdown file. | -| `HIDE_ITEMS_CLOSED_COUNT` | False | False | If set to `true`, the number of items closed metric will not be displayed in the generated Markdown file. | -| `HIDE_LABEL_METRICS` | False | False | If set to `true`, the time in label metrics will not be displayed in the generated Markdown file. | -| `HIDE_TIME_TO_ANSWER` | False | False | If set to `true`, the time to answer a discussion will not be displayed in the generated Markdown file. | -| `HIDE_TIME_TO_CLOSE` | False | False | If set to `true`, the time to close will not be displayed in the generated Markdown file. | -| `HIDE_TIME_TO_FIRST_RESPONSE` | False | False | If set to `true`, the time to first response will not be displayed in the generated Markdown file. | -| `IGNORE_USERS` | False | False | A comma separated list of users to ignore when calculating metrics. (ie. `IGNORE_USERS: 'user1,user2'`). To ignore bots, append `[bot]` to the user (ie. `IGNORE_USERS: 'github-actions[bot]'`) | -| `ENABLE_MENTOR_COUNT` | False | False | If set to 'TRUE' count number of comments users left on discussions, issues and PRs and display number of active mentors | -| `MIN_MENTOR_COMMENTS` | False | 10 | Minimum number of comments to count as a mentor | -| `MAX_COMMENTS_EVAL` | False | 20 | Maximum number of comments per thread to evaluate for mentor stats | -| `HEAVILY_INVOLVED_CUTOFF` | False | 3 | Cutoff after which a mentor's comments in one issue are no longer counted against their total score | -| `LABELS_TO_MEASURE` | False | `""` | A comma separated list of labels to measure how much time the label is applied. If not provided, no labels durations will be measured. Not compatible with discussions at this time. | -| `SEARCH_QUERY` | True | `""` | The query by which you can filter issues/PRs which must contain a `repo:`, `org:`, `owner:`, or a `user:` entry. For discussions, include `type:discussions` in the query. | +| field | required | default | description | +| ----------------------------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `GH_ENTERPRISE_URL` | False | `""` | URL of GitHub Enterprise instance to use for auth instead of github.com | +| `HIDE_AUTHOR` | False | False | If set to `true`, the author will not be displayed in the generated Markdown file. | +| `HIDE_ITEMS_CLOSED_COUNT` | False | False | If set to `true`, the number of items closed metric will not be displayed in the generated Markdown file. | +| `HIDE_LABEL_METRICS` | False | False | If set to `true`, the time in label metrics will not be displayed in the generated Markdown file. | +| `HIDE_TIME_TO_ANSWER` | False | False | If set to `true`, the time to answer a discussion will not be displayed in the generated Markdown file. | +| `HIDE_TIME_TO_CLOSE` | False | False | If set to `true`, the time to close will not be displayed in the generated Markdown file. | +| `HIDE_TIME_TO_FIRST_RESPONSE` | False | False | If set to `true`, the time to first response will not be displayed in the generated Markdown file. | +| `IGNORE_USERS` | False | False | A comma separated list of users to ignore when calculating metrics. (ie. `IGNORE_USERS: 'user1,user2'`). To ignore bots, append `[bot]` to the user (ie. `IGNORE_USERS: 'github-actions[bot]'`) | +| `ENABLE_MENTOR_COUNT` | False | False | If set to 'TRUE' count number of comments users left on discussions, issues and PRs and display number of active mentors | +| `MIN_MENTOR_COMMENTS` | False | 10 | Minimum number of comments to count as a mentor | +| `MAX_COMMENTS_EVAL` | False | 20 | Maximum number of comments per thread to evaluate for mentor stats | +| `HEAVILY_INVOLVED_CUTOFF` | False | 3 | Cutoff after which a mentor's comments in one issue are no longer counted against their total score | +| `LABELS_TO_MEASURE` | False | `""` | A comma separated list of labels to measure how much time the label is applied. If not provided, no labels durations will be measured. Not compatible with discussions at this time. | +| `SEARCH_QUERY` | True | `""` | The query by which you can filter issues/PRs which must contain a `repo:`, `org:`, `owner:`, or a `user:` entry. For discussions, include `type:discussions` in the query. | ## Further Documentation diff --git a/action.yml b/action.yml index 73a9942d..1a5179a0 100644 --- a/action.yml +++ b/action.yml @@ -1,10 +1,10 @@ --- -name: 'issue-metrics' -author: 'github' -description: 'A GitHub Action to report out issue metrics' +name: "issue-metrics" +author: "github" +description: "A GitHub Action to report out issue metrics" runs: - using: 'docker' - image: 'docker://ghcr.io/github/issue_metrics:v3' + using: "docker" + image: "docker://ghcr.io/github/issue_metrics:v3" branding: - icon: 'check-square' - color: 'white' + icon: "check-square" + color: "white" diff --git a/docs/dealing-with-large-issue-metrics.md b/docs/dealing-with-large-issue-metrics.md index c2247f63..f41337e7 100644 --- a/docs/dealing-with-large-issue-metrics.md +++ b/docs/dealing-with-large-issue-metrics.md @@ -10,6 +10,7 @@ To work around this limitation, the issue-metrics action detects the large file Since we don't want the action to fail, it has been designed to have the same name as usual for the first split file (issue_metrics.md) and then append a number to the name for the subsequent split files. You can choose one of the following strategies to deal with the split files: + - Create multiple issues, each with using the next split file in the sequence. - Upload the full file as an artifact and link to it in the issue body. - Create an issue and put the content of the split files as issue comments. diff --git a/docs/example-using-json-instead-markdown-output.md b/docs/example-using-json-instead-markdown-output.md index 3995b0fb..4fcfc769 100644 --- a/docs/example-using-json-instead-markdown-output.md +++ b/docs/example-using-json-instead-markdown-output.md @@ -7,7 +7,7 @@ name: Monthly issue metrics on: workflow_dispatch: schedule: - - cron: '3 2 1 * *' + - cron: "3 2 1 * *" permissions: contents: read @@ -21,14 +21,13 @@ jobs: pull-requests: read steps: - - name: Run issue-metrics tool - id: issue-metrics - uses: github/issue-metrics@v3 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SEARCH_QUERY: 'repo:owner/repo is:issue created:2023-05-01..2023-05-31 -reason:"not planned"' - - - name: Print output of issue metrics tool - run: echo "${{ steps.issue-metrics.outputs.metrics }}" + - name: Run issue-metrics tool + id: issue-metrics + uses: github/issue-metrics@v3 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SEARCH_QUERY: 'repo:owner/repo is:issue created:2023-05-01..2023-05-31 -reason:"not planned"' + - name: Print output of issue metrics tool + run: echo "${{ steps.issue-metrics.outputs.metrics }}" ``` diff --git a/docs/example-workflows.md b/docs/example-workflows.md index c0ee9d7c..31bc9e2a 100644 --- a/docs/example-workflows.md +++ b/docs/example-workflows.md @@ -9,7 +9,7 @@ name: Monthly issue metrics on: workflow_dispatch: schedule: - - cron: '3 2 1 * *' + - cron: "3 2 1 * *" permissions: contents: read @@ -23,34 +23,32 @@ jobs: pull-requests: read steps: + - name: Get dates for last month + shell: bash + run: | + # Calculate the first day of the previous month + first_day=$(date -d "last month" +%Y-%m-01) - - name: Get dates for last month - shell: bash - run: | - # Calculate the first day of the previous month - first_day=$(date -d "last month" +%Y-%m-01) - - # Calculate the last day of the previous month - last_day=$(date -d "$first_day +1 month -1 day" +%Y-%m-%d) - - #Set an environment variable with the date range - echo "$first_day..$last_day" - echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV" - - - name: Run issue-metrics tool - uses: github/issue-metrics@v3 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SEARCH_QUERY: 'repo:owner/repo is:issue created:${{ env.last_month }} -reason:"not planned"' - - - name: Create issue - uses: peter-evans/create-issue-from-file@v4 - with: - title: Monthly issue metrics report - token: ${{ secrets.GITHUB_TOKEN }} - content-filepath: ./issue_metrics.md - assignees: + # Calculate the last day of the previous month + last_day=$(date -d "$first_day +1 month -1 day" +%Y-%m-%d) + #Set an environment variable with the date range + echo "$first_day..$last_day" + echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV" + + - name: Run issue-metrics tool + uses: github/issue-metrics@v3 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SEARCH_QUERY: 'repo:owner/repo is:issue created:${{ env.last_month }} -reason:"not planned"' + + - name: Create issue + uses: peter-evans/create-issue-from-file@v4 + with: + title: Monthly issue metrics report + token: ${{ secrets.GITHUB_TOKEN }} + content-filepath: ./issue_metrics.md + assignees: ``` ## Fixed Time Example @@ -70,25 +68,23 @@ jobs: name: issue metrics runs-on: ubuntu-latest permissions: - issues: write - pull-requests: read + issues: write + pull-requests: read steps: + - name: Run issue-metrics tool + uses: github/issue-metrics@v3 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SEARCH_QUERY: 'repo:owner/repo is:issue created:2023-05-01..2023-05-31 -reason:"not planned"' - - name: Run issue-metrics tool - uses: github/issue-metrics@v3 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SEARCH_QUERY: 'repo:owner/repo is:issue created:2023-05-01..2023-05-31 -reason:"not planned"' - - - name: Create issue - uses: peter-evans/create-issue-from-file@v4 - with: - title: Monthly issue metrics report - token: ${{ secrets.GITHUB_TOKEN }} - content-filepath: ./issue_metrics.md - assignees: - + - name: Create issue + uses: peter-evans/create-issue-from-file@v4 + with: + title: Monthly issue metrics report + token: ${{ secrets.GITHUB_TOKEN }} + content-filepath: ./issue_metrics.md + assignees: ``` ## Multiple Repositories Example diff --git a/docs/measure-time.md b/docs/measure-time.md index fb64bcb6..8d3cae9b 100644 --- a/docs/measure-time.md +++ b/docs/measure-time.md @@ -23,22 +23,20 @@ jobs: pull-requests: read steps: - - - name: Run issue-metrics tool - uses: github/issue-metrics@v3 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - LABELS_TO_MEASURE: 'waiting-for-manager-approval,waiting-for-security-review' - SEARCH_QUERY: 'repo:owner/repo is:issue created:2023-05-01..2023-05-31 -reason:"not planned"' - - - name: Create issue - uses: peter-evans/create-issue-from-file@v4 - with: - title: Monthly issue metrics report - token: ${{ secrets.GITHUB_TOKEN }} - content-filepath: ./issue_metrics.md - assignees: - + - name: Run issue-metrics tool + uses: github/issue-metrics@v3 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + LABELS_TO_MEASURE: "waiting-for-manager-approval,waiting-for-security-review" + SEARCH_QUERY: 'repo:owner/repo is:issue created:2023-05-01..2023-05-31 -reason:"not planned"' + + - name: Create issue + uses: peter-evans/create-issue-from-file@v4 + with: + title: Monthly issue metrics report + token: ${{ secrets.GITHUB_TOKEN }} + content-filepath: ./issue_metrics.md + assignees: ``` then the report will look like this: @@ -46,22 +44,21 @@ then the report will look like this: ```markdown # Issue Metrics -| Metric | Value | -| --- | ---: | -| Average time to first response | 0:50:44.666667 | -| Average time to close | 6 days, 7:08:52 | -| Average time to answer | 1 day | -| Average time spent in waiting-for-manager-approval | 0:00:41 | -| Average time spent in waiting-for-security-review | 2 days, 4:25:03 | -| Number of items that remain open | 2 | -| Number of items closed | 1 | -| Total number of items created | 3 | - -| Title | URL | Time to first response | Time to close | Time to answer | Time spent in waiting-for-manager-approval | Time spent in waiting-for-security-review | -| --- | --- | --- | --- | --- | --- | --- | -| Pull Request Title 1 | https://github.com/user/repo/pulls/1 | 0:05:26 | None | None | None | None | -| Issue Title 2 | https://github.com/user/repo/issues/2 | 2:26:07 | None | None | 0:00:41 | 2 days, 4:25:03 | - +| Metric | Value | +| -------------------------------------------------- | --------------: | +| Average time to first response | 0:50:44.666667 | +| Average time to close | 6 days, 7:08:52 | +| Average time to answer | 1 day | +| Average time spent in waiting-for-manager-approval | 0:00:41 | +| Average time spent in waiting-for-security-review | 2 days, 4:25:03 | +| Number of items that remain open | 2 | +| Number of items closed | 1 | +| Total number of items created | 3 | + +| Title | URL | Time to first response | Time to close | Time to answer | Time spent in waiting-for-manager-approval | Time spent in waiting-for-security-review | +| -------------------- | ------------------------------------- | ---------------------- | ------------- | -------------- | ------------------------------------------ | ----------------------------------------- | +| Pull Request Title 1 | https://github.com/user/repo/pulls/1 | 0:05:26 | None | None | None | None | +| Issue Title 2 | https://github.com/user/repo/issues/2 | 2:26:07 | None | None | 0:00:41 | 2 days, 4:25:03 | ``` ## Example issue_metrics.md output @@ -71,21 +68,20 @@ Here is the output with no hidden columns: ```markdown # Issue Metrics -| Metric | Value | -| --- | ---: | -| Average time to first response | 0:50:44.666667 | -| Average time to close | 6 days, 7:08:52 | -| Average time to answer | 1 day | -| Number of items that remain open | 2 | -| Number of items closed | 1 | -| Total number of items created | 3 | - -| Title | URL | Time to first response | Time to close | Time to answer | -| --- | --- | --- | --- | --- | -| Discussion Title 1 | https://github.com/user/repo/discussions/1 | 0:00:41 | 6 days, 7:08:52 | 1 day | -| Pull Request Title 2 | https://github.com/user/repo/pulls/2 | 0:05:26 | None | None | -| Issue Title 3 | https://github.com/user/repo/issues/3 | 2:26:07 | None | None | - +| Metric | Value | +| -------------------------------- | --------------: | +| Average time to first response | 0:50:44.666667 | +| Average time to close | 6 days, 7:08:52 | +| Average time to answer | 1 day | +| Number of items that remain open | 2 | +| Number of items closed | 1 | +| Total number of items created | 3 | + +| Title | URL | Time to first response | Time to close | Time to answer | +| -------------------- | ------------------------------------------ | ---------------------- | --------------- | -------------- | +| Discussion Title 1 | https://github.com/user/repo/discussions/1 | 0:00:41 | 6 days, 7:08:52 | 1 day | +| Pull Request Title 2 | https://github.com/user/repo/pulls/2 | 0:05:26 | None | None | +| Issue Title 3 | https://github.com/user/repo/issues/3 | 2:26:07 | None | None | ``` Here is the output with all hidable columns hidden: @@ -93,16 +89,15 @@ Here is the output with all hidable columns hidden: ```markdown # Issue Metrics -| Metric | Value | -| --- | ---: | -| Number of items that remain open | 2 | -| Number of items closed | 1 | -| Total number of items created | 3 | - -| Title | URL | -| --- | --- | -| Discussion Title 1 | https://github.com/user/repo/discussions/1 | -| Pull Request Title 2 | https://github.com/user/repo/pulls/2 | -| Issue Title 3 | https://github.com/user/repo/issues/3 | - +| Metric | Value | +| -------------------------------- | ----: | +| Number of items that remain open | 2 | +| Number of items closed | 1 | +| Total number of items created | 3 | + +| Title | URL | +| -------------------- | ------------------------------------------ | +| Discussion Title 1 | https://github.com/user/repo/discussions/1 | +| Pull Request Title 2 | https://github.com/user/repo/pulls/2 | +| Issue Title 3 | https://github.com/user/repo/issues/3 | ``` From 481df30737406bfcf43414824540450777afae25 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 17:14:20 +0000 Subject: [PATCH 123/358] chore(deps): bump mypy from 1.11.1 to 1.11.2 in the dependencies group Bumps the dependencies group with 1 update: [mypy](https://github.com/python/mypy). Updates `mypy` from 1.11.1 to 1.11.2 - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.11.1...v1.11.2) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 9bcd7a2d..48c8bbac 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,6 +1,6 @@ black==24.8.0 flake8==7.1.1 -mypy==1.11.1 +mypy==1.11.2 mypy-extensions==1.0.0 pylint==3.2.6 pytest==8.3.2 From 20111dd5508f6d5faf85e8b292e597137291cd5b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 17:22:24 +0000 Subject: [PATCH 124/358] chore(deps): bump the dependencies group with 2 updates Bumps the dependencies group with 2 updates: [github/contributors](https://github.com/github/contributors) and [github/codeql-action](https://github.com/github/codeql-action). Updates `github/contributors` from 1.4.3 to 1.5.0 - [Release notes](https://github.com/github/contributors/releases) - [Commits](https://github.com/github/contributors/compare/135b0430e856ade27175cbd1d4e1e11b0dd8ef95...1286dc8d6904a9a7f735e28b7503be164fb7d4b9) Updates `github/codeql-action` from 3.26.3 to 3.26.5 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/883d8588e56d1753a8a58c1c86e88976f0c23449...2c779ab0d087cd7fe7b826087247c2c81f27bfa6) --- updated-dependencies: - dependency-name: github/contributors dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/contributor_report.yaml | 2 +- .github/workflows/scorecard.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/contributor_report.yaml b/.github/workflows/contributor_report.yaml index 885d47be..62ef6141 100644 --- a/.github/workflows/contributor_report.yaml +++ b/.github/workflows/contributor_report.yaml @@ -27,7 +27,7 @@ jobs: echo "END_DATE=$end_date" >> "$GITHUB_ENV" - name: Run contributor action - uses: github/contributors@135b0430e856ade27175cbd1d4e1e11b0dd8ef95 + uses: github/contributors@1286dc8d6904a9a7f735e28b7503be164fb7d4b9 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} START_DATE: ${{ env.START_DATE }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 7312060f..8cd73f7e 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@883d8588e56d1753a8a58c1c86e88976f0c23449 # v3.24.9 + uses: github/codeql-action/upload-sarif@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.24.9 with: sarif_file: results.sarif From d495fd2926c0bc755e8a53cdb694bcf54cacda1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 17:37:34 +0000 Subject: [PATCH 125/358] chore(deps): bump the dependencies group with 4 updates Bumps the dependencies group with 4 updates: [super-linter/super-linter](https://github.com/super-linter/super-linter), [actions/setup-python](https://github.com/actions/setup-python), [actions/upload-artifact](https://github.com/actions/upload-artifact) and [github/codeql-action](https://github.com/github/codeql-action). Updates `super-linter/super-linter` from 7.0.0 to 7.1.0 - [Release notes](https://github.com/super-linter/super-linter/releases) - [Changelog](https://github.com/super-linter/super-linter/blob/main/CHANGELOG.md) - [Commits](https://github.com/super-linter/super-linter/compare/02a1172d274f021e4c70f66e23f1085eadd1064b...b92721f792f381cedc002ecdbb9847a15ece5bb8) Updates `actions/setup-python` from 5.1.1 to 5.2.0 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/39cd14951b08e74b54015e9e001cdefcf80e669f...f677139bbe7f9c59b41e40162b753c062f5d49a3) Updates `actions/upload-artifact` from 4.3.6 to 4.4.0 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/834a144ee995460fba8ed112a2fc961b36a5ec5a...50769540e7f4bd5e21e526ee35c689e35e0d6874) Updates `github/codeql-action` from 3.26.5 to 3.26.6 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/2c779ab0d087cd7fe7b826087247c2c81f27bfa6...4dd16135b69a43b6c8efb853346f8437d92d3c93) --- updated-dependencies: - dependency-name: super-linter/super-linter dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/linter.yaml | 2 +- .github/workflows/python-package.yml | 2 +- .github/workflows/scorecard.yml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index 9a918d6e..ecbc5c80 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -28,7 +28,7 @@ jobs: python -m pip install --upgrade pip pip install -r requirements.txt -r requirements-test.txt - name: Lint Code Base - uses: super-linter/super-linter@02a1172d274f021e4c70f66e23f1085eadd1064b + uses: super-linter/super-linter@b92721f792f381cedc002ecdbb9847a15ece5bb8 env: DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index e8058320..f56a0dc9 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -23,7 +23,7 @@ jobs: steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f + uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 8cd73f7e..989b7d08 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -36,12 +36,12 @@ jobs: results_format: sarif publish_results: true - name: "Upload artifact" - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: SARIF file path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.24.9 + uses: github/codeql-action/upload-sarif@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.24.9 with: sarif_file: results.sarif From 2af01838d7480347c156b2863e0f32e01afeffdb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 18:00:14 +0000 Subject: [PATCH 126/358] chore(deps): bump pylint from 3.2.6 to 3.2.7 in the dependencies group Bumps the dependencies group with 1 update: [pylint](https://github.com/pylint-dev/pylint). Updates `pylint` from 3.2.6 to 3.2.7 - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v3.2.6...v3.2.7) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 48c8bbac..a0691289 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,7 +2,7 @@ black==24.8.0 flake8==7.1.1 mypy==1.11.2 mypy-extensions==1.0.0 -pylint==3.2.6 +pylint==3.2.7 pytest==8.3.2 pytest-cov==5.0.0 types-pytz==2024.1.0.20240417 From bf1686ba7f4bf9d18bed498016f5b690187d3d88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:53:29 +0000 Subject: [PATCH 127/358] chore(deps): bump python from `59c7332` to `c24c34b` Bumps python from `59c7332` to `c24c34b`. --- updated-dependencies: - dependency-name: python dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9e4294b5..9b4c36f4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.12-slim@sha256:59c7332a4a24373861c4a5f0eec2c92b87e3efeb8ddef011744ef9a751b1d11c +FROM python:3.12-slim@sha256:c24c34b502635f1f7c4e99dc09a2cbd85d480b7dcfd077198c6b5af138906390 LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From fa5e0a634832504a39f13b37d29d4374f33f61c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:58:41 +0000 Subject: [PATCH 128/358] chore(deps): bump numpy from 2.1.0 to 2.1.1 in the dependencies group Bumps the dependencies group with 1 update: [numpy](https://github.com/numpy/numpy). Updates `numpy` from 2.1.0 to 2.1.1 - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.1.0...v2.1.1) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4690c546..0f974cf3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ github3.py==4.0.1 -numpy==2.1.0 +numpy==2.1.1 python-dotenv==1.0.1 pytz==2024.1 requests==2.32.3 From 6005235f5327b54ebf9a9a1f7ffaf7a3c00ebec0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:58:43 +0000 Subject: [PATCH 129/358] chore(deps): bump types-requests from 2.32.0.20240712 to 2.32.0.20240907 Bumps [types-requests](https://github.com/python/typeshed) from 2.32.0.20240712 to 2.32.0.20240907. - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index a0691289..c8e25cd5 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -6,4 +6,4 @@ pylint==3.2.7 pytest==8.3.2 pytest-cov==5.0.0 types-pytz==2024.1.0.20240417 -types-requests==2.32.0.20240712 +types-requests==2.32.0.20240907 From ae5f02f62dae0c6aa678f30fe362e8a52f8a0012 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 17:25:23 +0000 Subject: [PATCH 130/358] chore(deps): bump python from `c24c34b` to `15bad98` Bumps python from `c24c34b` to `15bad98`. --- updated-dependencies: - dependency-name: python dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9b4c36f4..c0f9e81e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.12-slim@sha256:c24c34b502635f1f7c4e99dc09a2cbd85d480b7dcfd077198c6b5af138906390 +FROM python:3.12-slim@sha256:15bad989b293be1dd5eb26a87ecacadaee1559f98e29f02bf6d00c8d86129f39 LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From d721b2ec13aaaa599a38b56635d90270f9660410 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Mon, 16 Sep 2024 13:11:23 -0500 Subject: [PATCH 131/358] fix: docker container image build switched from git-all to just git (pinned) Signed-off-by: jmeridth --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index c0f9e81e..b8be812a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ COPY requirements.txt *.py /action/workspace/ RUN python3 -m pip install --no-cache-dir -r requirements.txt \ && apt-get -y update \ - && apt-get -y install --no-install-recommends git-all=1:2.39.2-1.1 \ + && apt-get -y install --no-install-recommends git=1:2.39.5-0+deb12u1 \ && rm -rf /var/lib/apt/lists/* CMD ["/action/workspace/issue_metrics.py"] From e87f057b01068718e42c1d0136377ba81902d45a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 22:18:08 +0000 Subject: [PATCH 132/358] chore(deps): bump the dependencies group with 3 updates Bumps the dependencies group with 3 updates: [pytest](https://github.com/pytest-dev/pytest), [types-pytz](https://github.com/python/typeshed) and [pytz](https://github.com/stub42/pytz). Updates `pytest` from 8.3.2 to 8.3.3 - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.3.2...8.3.3) Updates `types-pytz` from 2024.1.0.20240417 to 2024.2.0.20240913 - [Commits](https://github.com/python/typeshed/commits) Updates `pytz` from 2024.1 to 2024.2 - [Release notes](https://github.com/stub42/pytz/releases) - [Commits](https://github.com/stub42/pytz/compare/release_2024.1...release_2024.2) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: types-pytz dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: pytz dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 4 ++-- requirements.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index c8e25cd5..70d45e0b 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -3,7 +3,7 @@ flake8==7.1.1 mypy==1.11.2 mypy-extensions==1.0.0 pylint==3.2.7 -pytest==8.3.2 +pytest==8.3.3 pytest-cov==5.0.0 -types-pytz==2024.1.0.20240417 +types-pytz==2024.2.0.20240913 types-requests==2.32.0.20240907 diff --git a/requirements.txt b/requirements.txt index 0f974cf3..b0a3b065 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ github3.py==4.0.1 numpy==2.1.1 python-dotenv==1.0.1 -pytz==2024.1 +pytz==2024.2 requests==2.32.3 From 386533943475e17e239538a8aacf80db85968889 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 22:46:28 +0000 Subject: [PATCH 133/358] chore(deps): bump types-requests from 2.32.0.20240907 to 2.32.0.20240914 Bumps [types-requests](https://github.com/python/typeshed) from 2.32.0.20240907 to 2.32.0.20240914. - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 70d45e0b..ea81411f 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -6,4 +6,4 @@ pylint==3.2.7 pytest==8.3.3 pytest-cov==5.0.0 types-pytz==2024.2.0.20240913 -types-requests==2.32.0.20240907 +types-requests==2.32.0.20240914 From def98602af3ba289385f96224c3657e9d44d7490 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Sep 2024 00:05:17 +0000 Subject: [PATCH 134/358] chore(deps): bump github/codeql-action in the dependencies group Bumps the dependencies group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 3.26.6 to 3.26.7 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/4dd16135b69a43b6c8efb853346f8437d92d3c93...8214744c546c1e5c8f03dde8fab3a7353211988d) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 989b7d08..0fe5a1d0 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.24.9 + uses: github/codeql-action/upload-sarif@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.24.9 with: sarif_file: results.sarif From ffd46d441fd7ca25df361d94d38afe26654c569c Mon Sep 17 00:00:00 2001 From: jmeridth Date: Wed, 18 Sep 2024 16:20:23 -0500 Subject: [PATCH 135/358] fix: typo in README Signed-off-by: jmeridth --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a22b398c..f28834f2 100644 --- a/README.md +++ b/README.md @@ -177,4 +177,4 @@ We would ❤️ contributions to improve this action. Please see [CONTRIBUTING.m ## More OSPO Tools -Looking for more resources for your open source program office (OSPO)? Check out the [`github-ospo`](https://github.com/github/github-ospo) repoistory for a variety of tools designed to support your needs. +Looking for more resources for your open source program office (OSPO)? Check out the [`github-ospo`](https://github.com/github/github-ospo) repository for a variety of tools designed to support your needs. From c03a0d7a22f84315d1c8bffb279c9f5a3ad514b9 Mon Sep 17 00:00:00 2001 From: willcl-ark Date: Tue, 17 Sep 2024 10:43:54 +0100 Subject: [PATCH 136/358] feat: add non-mentioning links option --- README.md | 1 + config.py | 5 +++++ issue_metrics.py | 2 ++ markdown_writer.py | 9 ++++++++- test_config.py | 4 ++++ test_markdown_writer.py | 8 +++++--- 6 files changed, 25 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f28834f2..030ef4df 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ This action can be configured to authenticate with GitHub App Installation or Pe | `MAX_COMMENTS_EVAL` | False | 20 | Maximum number of comments per thread to evaluate for mentor stats | | `HEAVILY_INVOLVED_CUTOFF` | False | 3 | Cutoff after which a mentor's comments in one issue are no longer counted against their total score | | `LABELS_TO_MEASURE` | False | `""` | A comma separated list of labels to measure how much time the label is applied. If not provided, no labels durations will be measured. Not compatible with discussions at this time. | +| `NON_MENTIONING_LINKS` | False | False | If set to `true`, will use non-mentioning GitHub links to avoid linking to the generated issue from the source repository. Links of the form `https://www.github.com` will be used. | | `SEARCH_QUERY` | True | `""` | The query by which you can filter issues/PRs which must contain a `repo:`, `org:`, `owner:`, or a `user:` entry. For discussions, include `type:discussions` in the query. | ## Further Documentation diff --git a/config.py b/config.py index 5186d441..9056abbe 100644 --- a/config.py +++ b/config.py @@ -62,6 +62,7 @@ def __init__( max_comments_eval: str, heavily_involved_cutoff: str, search_query: str, + non_mentioning_links: bool, ): self.gh_app_id = gh_app_id self.gh_app_installation_id = gh_app_installation_id @@ -81,6 +82,7 @@ def __init__( self.max_comments_eval = max_comments_eval self.heavily_involved_cutoff = heavily_involved_cutoff self.search_query = search_query + self.non_mentioning_links = non_mentioning_links def __repr__(self): return ( @@ -103,6 +105,7 @@ def __repr__(self): f"{self.max_comments_eval}," f"{self.heavily_involved_cutoff}," f"{self.search_query}" + f"{self.non_mentioning_links}" ) @@ -195,6 +198,7 @@ def get_env_vars(test: bool = False) -> EnvVars: min_mentor_comments = os.getenv("MIN_MENTOR_COMMENTS", "10") max_comments_eval = os.getenv("MAX_COMMENTS_EVAL", "20") heavily_involved_cutoff = os.getenv("HEAVILY_INVOLVED_CUTOFF", "3") + non_mentioning_links = get_bool_env_var("NON_MENTIONING_LINKS", False) return EnvVars( gh_app_id, @@ -215,4 +219,5 @@ def get_env_vars(test: bool = False) -> EnvVars: max_comments_eval, heavily_involved_cutoff, search_query, + non_mentioning_links, ) diff --git a/issue_metrics.py b/issue_metrics.py index 3d62d30a..88c827fc 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -306,6 +306,7 @@ def main(): token = env_vars.gh_token ignore_users = env_vars.ignore_users hide_items_closed_count = env_vars.hide_items_closed_count + non_mentioning_links = env_vars.non_mentioning_links gh_app_id = env_vars.gh_app_id gh_app_installation_id = env_vars.gh_app_installation_id @@ -414,6 +415,7 @@ def main(): labels, search_query, hide_items_closed_count, + non_mentioning_links, ) max_char_count = 65535 diff --git a/markdown_writer.py b/markdown_writer.py index 5e3e3008..d2cde396 100644 --- a/markdown_writer.py +++ b/markdown_writer.py @@ -85,6 +85,7 @@ def write_to_markdown( search_query=None, hide_label_metrics=False, hide_items_closed_count=False, + non_mentioning_links=False, ) -> None: """Write the issues with metrics to a markdown file. @@ -160,7 +161,13 @@ def write_to_markdown( # Replace any whitespace issue.title = issue.title.strip() - file.write(f"| " f"{issue.title} | " f"{issue.html_url} |") + if non_mentioning_links: + file.write( + f"| {issue.title} | " + f"{issue.html_url.replace('https://github.com', 'https://www.github.com')} |" + ) + else: + file.write(f"| {issue.title} | " f"{issue.html_url} |") if "Author" in columns: file.write(f" [{issue.author}](https://github.com/{issue.author}) |") if "Time to first response" in columns: diff --git a/test_config.py b/test_config.py index 6c2abefa..e52326bd 100644 --- a/test_config.py +++ b/test_config.py @@ -126,6 +126,7 @@ def test_get_env_vars_with_github_app(self): "20", "3", SEARCH_QUERY, + False, ) result = get_env_vars(True) self.assertEqual(str(result), str(expected_result)) @@ -171,6 +172,7 @@ def test_get_env_vars_with_token(self): "20", "3", SEARCH_QUERY, + False, ) result = get_env_vars(True) self.assertEqual(str(result), str(expected_result)) @@ -250,6 +252,7 @@ def test_get_env_vars_optional_values(self): 20, 3, SEARCH_QUERY, + False, ) result = get_env_vars(True) self.assertEqual(str(result), str(expected_result)) @@ -286,6 +289,7 @@ def test_get_env_vars_optionals_are_defaulted(self): "20", "3", SEARCH_QUERY, + False, ) result = get_env_vars(True) self.assertEqual(str(result), str(expected_result)) diff --git a/test_markdown_writer.py b/test_markdown_writer.py index 4328f70b..0eb5e80e 100644 --- a/test_markdown_writer.py +++ b/test_markdown_writer.py @@ -254,10 +254,11 @@ def test_write_to_markdown_no_issues(self): "HIDE_TIME_TO_CLOSE": "True", "HIDE_TIME_TO_ANSWER": "True", "HIDE_LABEL_METRICS": "True", + "NON_MENTIONING_LINKS": "True", }, ) class TestWriteToMarkdownWithEnv(unittest.TestCase): - """Test the write_to_markdown function with the HIDE* environment variables set.""" + """Test the write_to_markdown function with the HIDE* and NON_MENTIONING_LINKS environment variables set.""" def test_writes_markdown_file_with_non_hidden_columns_only(self): """ @@ -314,6 +315,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): search_query="repo:user/repo is:issue", hide_label_metrics=True, hide_items_closed_count=True, + non_mentioning_links=True, ) # Check that the function writes the correct markdown file @@ -328,8 +330,8 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): "| Total number of items created | 2 |\n\n" "| Title | URL | Author |\n" "| --- | --- | --- |\n" - "| Issue 1 | https://github.com/user/repo/issues/1 | [alice](https://github.com/alice) |\n" - "| Issue 2 | https://github.com/user/repo/issues/2 | [bob](https://github.com/bob) |\n\n" + "| Issue 1 | https://www.github.com/user/repo/issues/1 | [alice](https://github.com/alice) |\n" + "| Issue 2 | https://www.github.com/user/repo/issues/2 | [bob](https://github.com/bob) |\n\n" "_This report was generated with the [Issue Metrics Action](https://github.com/github/issue-metrics)_\n" "Search query used to find these items: `repo:user/repo is:issue`\n" ) From 82a5a61b79abfc421e1e728dc6d1f7793780ec1b Mon Sep 17 00:00:00 2001 From: jmeridth Date: Thu, 19 Sep 2024 11:13:25 -0500 Subject: [PATCH 137/358] feat: REPORT_TITLE and OUTPUT_FILE environment variables Closes #160 - [x] Adds the ability for users to designate custom report title and output files. - Defaults for each: - REPORT_TITLE: "Issue Metrics" - OUTPUT_FILE: "issue_metrics.md" if markdown and "issue_metrics.json" if json - [x] Update `.env-example` and include changes there and in docs we forgot in #370 Signed-off-by: jmeridth Co-authored-by: Sebastian Spier Co-authored-by: Zack Koppert --- .env-example | 3 +++ README.md | 36 +++++++++++++++++++----------------- config.py | 14 ++++++++++++++ issue_metrics.py | 6 ++++++ json_writer.py | 5 ++++- markdown_writer.py | 10 ++++++++-- test_config.py | 22 +++++++++++++++++++++- test_json_writer.py | 2 ++ test_markdown_writer.py | 18 +++++++++++++++++- 9 files changed, 94 insertions(+), 22 deletions(-) diff --git a/.env-example b/.env-example index c905ee3d..c7d8193f 100644 --- a/.env-example +++ b/.env-example @@ -11,4 +11,7 @@ HIDE_TIME_TO_CLOSE = "false" HIDE_TIME_TO_FIRST_RESPONSE = "false" IGNORE_USERS = "user1,user2" LABELS_TO_MEASURE = "waiting-for-review,waiting-for-manager" +NON_MENTIONING_LINKS = "false" +OUTPUT_FILE = "" +REPORT_TITLE = "Issue Metrics" SEARCH_QUERY = "repo:owner/repo is:open is:issue" diff --git a/README.md b/README.md index 030ef4df..39c15f42 100644 --- a/README.md +++ b/README.md @@ -139,23 +139,25 @@ This action can be configured to authenticate with GitHub App Installation or Pe #### Other Configuration Options -| field | required | default | description | -| ----------------------------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `GH_ENTERPRISE_URL` | False | `""` | URL of GitHub Enterprise instance to use for auth instead of github.com | -| `HIDE_AUTHOR` | False | False | If set to `true`, the author will not be displayed in the generated Markdown file. | -| `HIDE_ITEMS_CLOSED_COUNT` | False | False | If set to `true`, the number of items closed metric will not be displayed in the generated Markdown file. | -| `HIDE_LABEL_METRICS` | False | False | If set to `true`, the time in label metrics will not be displayed in the generated Markdown file. | -| `HIDE_TIME_TO_ANSWER` | False | False | If set to `true`, the time to answer a discussion will not be displayed in the generated Markdown file. | -| `HIDE_TIME_TO_CLOSE` | False | False | If set to `true`, the time to close will not be displayed in the generated Markdown file. | -| `HIDE_TIME_TO_FIRST_RESPONSE` | False | False | If set to `true`, the time to first response will not be displayed in the generated Markdown file. | -| `IGNORE_USERS` | False | False | A comma separated list of users to ignore when calculating metrics. (ie. `IGNORE_USERS: 'user1,user2'`). To ignore bots, append `[bot]` to the user (ie. `IGNORE_USERS: 'github-actions[bot]'`) | -| `ENABLE_MENTOR_COUNT` | False | False | If set to 'TRUE' count number of comments users left on discussions, issues and PRs and display number of active mentors | -| `MIN_MENTOR_COMMENTS` | False | 10 | Minimum number of comments to count as a mentor | -| `MAX_COMMENTS_EVAL` | False | 20 | Maximum number of comments per thread to evaluate for mentor stats | -| `HEAVILY_INVOLVED_CUTOFF` | False | 3 | Cutoff after which a mentor's comments in one issue are no longer counted against their total score | -| `LABELS_TO_MEASURE` | False | `""` | A comma separated list of labels to measure how much time the label is applied. If not provided, no labels durations will be measured. Not compatible with discussions at this time. | -| `NON_MENTIONING_LINKS` | False | False | If set to `true`, will use non-mentioning GitHub links to avoid linking to the generated issue from the source repository. Links of the form `https://www.github.com` will be used. | -| `SEARCH_QUERY` | True | `""` | The query by which you can filter issues/PRs which must contain a `repo:`, `org:`, `owner:`, or a `user:` entry. For discussions, include `type:discussions` in the query. | +| field | required | default | description | +| ----------------------------- | -------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `GH_ENTERPRISE_URL` | False | `""` | URL of GitHub Enterprise instance to use for auth instead of github.com | +| `HIDE_AUTHOR` | False | False | If set to `true`, the author will not be displayed in the generated Markdown file. | +| `HIDE_ITEMS_CLOSED_COUNT` | False | False | If set to `true`, the number of items closed metric will not be displayed in the generated Markdown file. | +| `HIDE_LABEL_METRICS` | False | False | If set to `true`, the time in label metrics will not be displayed in the generated Markdown file. | +| `HIDE_TIME_TO_ANSWER` | False | False | If set to `true`, the time to answer a discussion will not be displayed in the generated Markdown file. | +| `HIDE_TIME_TO_CLOSE` | False | False | If set to `true`, the time to close will not be displayed in the generated Markdown file. | +| `HIDE_TIME_TO_FIRST_RESPONSE` | False | False | If set to `true`, the time to first response will not be displayed in the generated Markdown file. | +| `IGNORE_USERS` | False | False | A comma separated list of users to ignore when calculating metrics. (ie. `IGNORE_USERS: 'user1,user2'`). To ignore bots, append `[bot]` to the user (ie. `IGNORE_USERS: 'github-actions[bot]'`) | +| `ENABLE_MENTOR_COUNT` | False | False | If set to 'TRUE' count number of comments users left on discussions, issues and PRs and display number of active mentors | +| `MIN_MENTOR_COMMENTS` | False | 10 | Minimum number of comments to count as a mentor | +| `MAX_COMMENTS_EVAL` | False | 20 | Maximum number of comments per thread to evaluate for mentor stats | +| `HEAVILY_INVOLVED_CUTOFF` | False | 3 | Cutoff after which a mentor's comments in one issue are no longer counted against their total score | +| `LABELS_TO_MEASURE` | False | `""` | A comma separated list of labels to measure how much time the label is applied. If not provided, no labels durations will be measured. Not compatible with discussions at this time. | +| `NON_MENTIONING_LINKS` | False | False | If set to `true`, will use non-mentioning GitHub links to avoid linking to the generated issue from the source repository. Links of the form `https://www.github.com` will be used. | +| `OUTPUT_FILE` | False | `issue_metrics.md` or `issue_metrics.json` | Output filename. | +| `REPORT_TITLE` | False | `"Issue Metrics"` | Title to have on the report issue. | +| `SEARCH_QUERY` | True | `""` | The query by which you can filter issues/PRs which must contain a `repo:`, `org:`, `owner:`, or a `user:` entry. For discussions, include `type:discussions` in the query. | ## Further Documentation diff --git a/config.py b/config.py index 9056abbe..b45093a8 100644 --- a/config.py +++ b/config.py @@ -40,6 +40,9 @@ class EnvVars: max_comments_eval (str): If set, defines the maximum number of comments to look at for mentor evaluation heavily_involved_cutoff (str): If set, defines the cutoff after which heavily involved commentors in search_query (str): Search query used to filter issues/prs/discussions on GitHub + non_mentioning_links (bool): If set to TRUE, links do not cause a notification in the desitnation repository + report_title (str): The title of the report + output_file (str): The name of the file to write the report to """ def __init__( @@ -63,6 +66,8 @@ def __init__( heavily_involved_cutoff: str, search_query: str, non_mentioning_links: bool, + report_title: str, + output_file: str, ): self.gh_app_id = gh_app_id self.gh_app_installation_id = gh_app_installation_id @@ -83,6 +88,8 @@ def __init__( self.heavily_involved_cutoff = heavily_involved_cutoff self.search_query = search_query self.non_mentioning_links = non_mentioning_links + self.report_title = report_title + self.output_file = output_file def __repr__(self): return ( @@ -106,6 +113,8 @@ def __repr__(self): f"{self.heavily_involved_cutoff}," f"{self.search_query}" f"{self.non_mentioning_links}" + f"{self.report_title}" + f"{self.output_file}" ) @@ -187,6 +196,9 @@ def get_env_vars(test: bool = False) -> EnvVars: if ignore_users: ignore_users_list = ignore_users.split(",") + report_title = os.getenv("REPORT_TITLE", "Issue Metrics") + output_file = os.getenv("OUTPUT_FILE", "") + # Hidden columns hide_author = get_bool_env_var("HIDE_AUTHOR", False) hide_items_closed_count = get_bool_env_var("HIDE_ITEMS_CLOSED_COUNT", False) @@ -220,4 +232,6 @@ def get_env_vars(test: bool = False) -> EnvVars: heavily_involved_cutoff, search_query, non_mentioning_links, + report_title, + output_file, ) diff --git a/issue_metrics.py b/issue_metrics.py index 88c827fc..d9aaf9a4 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -307,6 +307,8 @@ def main(): ignore_users = env_vars.ignore_users hide_items_closed_count = env_vars.hide_items_closed_count non_mentioning_links = env_vars.non_mentioning_links + report_title = env_vars.report_title + output_file = env_vars.output_file gh_app_id = env_vars.gh_app_id gh_app_installation_id = env_vars.gh_app_installation_id @@ -401,6 +403,8 @@ def main(): num_issues_closed, num_mentor_count, search_query, + report_title, + output_file, ) write_to_markdown( @@ -416,6 +420,8 @@ def main(): search_query, hide_items_closed_count, non_mentioning_links, + report_title, + output_file, ) max_char_count = 65535 diff --git a/json_writer.py b/json_writer.py index 1b40e01c..0b191dc4 100644 --- a/json_writer.py +++ b/json_writer.py @@ -9,6 +9,7 @@ num_issues_opened: int, num_issues_closed: int, search_query: str, + output_file: str, ) -> str: Write the issues with metrics to a json file. @@ -32,6 +33,7 @@ def write_to_json( num_issues_closed: Union[int, None], num_mentor_count: Union[int, None], search_query: str, + output_file: str, ) -> str: """ Write the issues with metrics to a JSON file called issue_metrics.json. @@ -168,7 +170,8 @@ def write_to_json( print(f"metrics={metrics_json}", file=file_handle) # Write the metrics to a JSON file - with open("issue_metrics.json", "w", encoding="utf-8") as file: + output_file_name = output_file if output_file else "issue_metrics.json" + with open(output_file_name, "w", encoding="utf-8") as file: json.dump(metrics, file, indent=4) return metrics_json diff --git a/markdown_writer.py b/markdown_writer.py index d2cde396..e9d8d3bf 100644 --- a/markdown_writer.py +++ b/markdown_writer.py @@ -86,6 +86,8 @@ def write_to_markdown( hide_label_metrics=False, hide_items_closed_count=False, non_mentioning_links=False, + report_title="", + output_file="", ) -> None: """Write the issues with metrics to a markdown file. @@ -105,14 +107,18 @@ def write_to_markdown( search_query (str): The search query used to find the issues. hide_label_metrics (bool): Represents whether the user has chosen to hide label metrics in the output hide_items_closed_count (bool): Represents whether the user has chosen to hide the number of items closed + non_mentioning_links (bool): Represents whether links do not cause a notification in the desitnation repository + report_title (str): The title of the report + output_file (str): The name of the file to write the report to Returns: None. """ columns = get_non_hidden_columns(labels) - with open("issue_metrics.md", "w", encoding="utf-8") as file: - file.write("# Issue Metrics\n\n") + output_file_name = output_file if output_file else "issue_metrics.md" + with open(output_file_name, "w", encoding="utf-8") as file: + file.write(f"# {report_title}\n\n") # If all the metrics are None, then there are no issues if not issues_with_metrics or len(issues_with_metrics) == 0: diff --git a/test_config.py b/test_config.py index e52326bd..3a963154 100644 --- a/test_config.py +++ b/test_config.py @@ -79,6 +79,9 @@ def setUp(self): "HIDE_TIME_TO_FIRST_RESPONSE", "IGNORE_USERS", "LABELS_TO_MEASURE", + "NON_MENTIONING_LINKS", + "OUTPUT_FILE", + "REPORT_TITLE", "SEARCH_QUERY", ] for key in env_keys: @@ -101,6 +104,9 @@ def setUp(self): "HIDE_TIME_TO_FIRST_RESPONSE": "", "IGNORE_USERS": "", "LABELS_TO_MEASURE": "", + "NON_MENTIONING_LINKS": "false", + "OUTPUT_FILE": "", + "REPORT_TITLE": "", "SEARCH_QUERY": SEARCH_QUERY, }, clear=True, @@ -127,6 +133,8 @@ def test_get_env_vars_with_github_app(self): "3", SEARCH_QUERY, False, + "", + "", ) result = get_env_vars(True) self.assertEqual(str(result), str(expected_result)) @@ -147,6 +155,9 @@ def test_get_env_vars_with_github_app(self): "HIDE_TIME_TO_FIRST_RESPONSE": "", "IGNORE_USERS": "", "LABELS_TO_MEASURE": "", + "NON_MENTIONING_LINKS": "false", + "OUTPUT_FILE": "", + "REPORT_TITLE": "", "SEARCH_QUERY": SEARCH_QUERY, }, clear=True, @@ -173,6 +184,8 @@ def test_get_env_vars_with_token(self): "3", SEARCH_QUERY, False, + "", + "", ) result = get_env_vars(True) self.assertEqual(str(result), str(expected_result)) @@ -228,6 +241,9 @@ def test_get_env_vars_missing_query(self): "HIDE_TIME_TO_FIRST_RESPONSE": "true", "IGNORE_USERS": "", "LABELS_TO_MEASURE": "waiting-for-review,waiting-for-manager", + "NON_MENTIONING_LINKS": "true", + "OUTPUT_FILE": "issue_metrics.md", + "REPORT_TITLE": "Issue Metrics", "SEARCH_QUERY": SEARCH_QUERY, }, ) @@ -252,7 +268,9 @@ def test_get_env_vars_optional_values(self): 20, 3, SEARCH_QUERY, - False, + True, + "Issue Metrics", + "issue_metrics.md", ) result = get_env_vars(True) self.assertEqual(str(result), str(expected_result)) @@ -290,6 +308,8 @@ def test_get_env_vars_optionals_are_defaulted(self): "3", SEARCH_QUERY, False, + "Issue Metrics", + "", ) result = get_env_vars(True) self.assertEqual(str(result), str(expected_result)) diff --git a/test_json_writer.py b/test_json_writer.py index fe1fe1b8..5dae93c8 100644 --- a/test_json_writer.py +++ b/test_json_writer.py @@ -115,6 +115,7 @@ def test_write_to_json(self): num_issues_closed=num_issues_closed, num_mentor_count=num_mentor_count, search_query="is:issue repo:owner/repo", + output_file="issue_metrics.json", ), json.dumps(expected_output), ) @@ -206,6 +207,7 @@ def test_write_to_json_with_no_response(self): num_issues_closed=num_issues_closed, num_mentor_count=num_mentor_count, search_query="is:issue repo:owner/repo", + output_file="issue_metrics.json", ), json.dumps(expected_output), ) diff --git a/test_markdown_writer.py b/test_markdown_writer.py index 0eb5e80e..3678d0ba 100644 --- a/test_markdown_writer.py +++ b/test_markdown_writer.py @@ -92,6 +92,8 @@ def test_write_to_markdown(self): num_mentor_count=num_mentor_count, labels=["bug"], search_query="is:issue is:open label:bug", + report_title="Issue Metrics", + output_file="issue_metrics.md", ) # Check that the function writes the correct markdown file @@ -191,6 +193,8 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): num_issues_closed=num_issues_closed, num_mentor_count=num_mentor_count, labels=["bug"], + report_title="Issue Metrics", + output_file="issue_metrics.md", ) # Check that the function writes the correct markdown file @@ -227,7 +231,17 @@ def test_write_to_markdown_no_issues(self): """Test that write_to_markdown writes the correct markdown file when no issues are found.""" # Call the function with no issues with patch("builtins.open", mock_open()) as mock_open_file: - write_to_markdown(None, None, None, None, None, None, None, None) + write_to_markdown( + None, + None, + None, + None, + None, + None, + None, + None, + report_title="Issue Metrics", + ) # Check that the file was written correctly expected_output = [ @@ -316,6 +330,8 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): hide_label_metrics=True, hide_items_closed_count=True, non_mentioning_links=True, + report_title="Issue Metrics", + output_file="issue_metrics.md", ) # Check that the function writes the correct markdown file From d12eabab3089de8613cdeddce3ca96cbd40424bf Mon Sep 17 00:00:00 2001 From: jmeridth Date: Thu, 19 Sep 2024 14:27:14 -0500 Subject: [PATCH 138/358] fix: too many arguments to write_to_json Had too many arguments in write_to_json call Signed-off-by: jmeridth --- issue_metrics.py | 1 - 1 file changed, 1 deletion(-) diff --git a/issue_metrics.py b/issue_metrics.py index d9aaf9a4..e6b84f8c 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -403,7 +403,6 @@ def main(): num_issues_closed, num_mentor_count, search_query, - report_title, output_file, ) From b410edbd2c0ad41178f7f7f35f1b65c05e31f1c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:05:59 +0000 Subject: [PATCH 139/358] chore(deps): bump pylint from 3.2.7 to 3.3.0 in the dependencies group Bumps the dependencies group with 1 update: [pylint](https://github.com/pylint-dev/pylint). Updates `pylint` from 3.2.7 to 3.3.0 - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v3.2.7...v3.3.0) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index ea81411f..15f17832 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,7 +2,7 @@ black==24.8.0 flake8==7.1.1 mypy==1.11.2 mypy-extensions==1.0.0 -pylint==3.2.7 +pylint==3.3.0 pytest==8.3.3 pytest-cov==5.0.0 types-pytz==2024.2.0.20240913 From 6d5e4f721a53912393c6e0a5f879d891d94e01ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:44:57 +0000 Subject: [PATCH 140/358] chore(deps): bump github/codeql-action in the dependencies group Bumps the dependencies group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 3.26.7 to 3.26.8 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/8214744c546c1e5c8f03dde8fab3a7353211988d...294a9d92911152fe08befb9ec03e240add280cb3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 0fe5a1d0..ea50b855 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.24.9 + uses: github/codeql-action/upload-sarif@294a9d92911152fe08befb9ec03e240add280cb3 # v3.24.9 with: sarif_file: results.sarif From ce411c0543449a0551b5b9ff9d483d43ae5d5a36 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Mon, 23 Sep 2024 21:46:46 -0700 Subject: [PATCH 141/358] fix: Remove ignore_users authored items from markdown results Signed-off-by: Zack Koppert --- README.md | 2 +- issue_metrics.py | 3 ++ test_issue_metrics.py | 96 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 96 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 39c15f42..6e92765d 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ This action can be configured to authenticate with GitHub App Installation or Pe | `HIDE_TIME_TO_ANSWER` | False | False | If set to `true`, the time to answer a discussion will not be displayed in the generated Markdown file. | | `HIDE_TIME_TO_CLOSE` | False | False | If set to `true`, the time to close will not be displayed in the generated Markdown file. | | `HIDE_TIME_TO_FIRST_RESPONSE` | False | False | If set to `true`, the time to first response will not be displayed in the generated Markdown file. | -| `IGNORE_USERS` | False | False | A comma separated list of users to ignore when calculating metrics. (ie. `IGNORE_USERS: 'user1,user2'`). To ignore bots, append `[bot]` to the user (ie. `IGNORE_USERS: 'github-actions[bot]'`) | +| `IGNORE_USERS` | False | False | A comma separated list of users to ignore when calculating metrics. (ie. `IGNORE_USERS: 'user1,user2'`). To ignore bots, append `[bot]` to the user (ie. `IGNORE_USERS: 'github-actions[bot]'`) Users in this list will also have their authored issues and pull requests removed from the markdown table. | | `ENABLE_MENTOR_COUNT` | False | False | If set to 'TRUE' count number of comments users left on discussions, issues and PRs and display number of active mentors | | `MIN_MENTOR_COMMENTS` | False | 10 | Minimum number of comments to count as a mentor | | `MAX_COMMENTS_EVAL` | False | 20 | Maximum number of comments per thread to evaluate for mentor stats | diff --git a/issue_metrics.py b/issue_metrics.py index e6b84f8c..3d176cfd 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -202,6 +202,9 @@ def get_per_issue_metrics( else: num_issues_open += 1 else: + if ignore_users and issue.user["login"] in ignore_users: # type: ignore + continue + issue_with_metrics = IssueWithMetrics( issue.title, # type: ignore issue.html_url, # type: ignore diff --git a/test_issue_metrics.py b/test_issue_metrics.py index 5059a88d..40e12445 100644 --- a/test_issue_metrics.py +++ b/test_issue_metrics.py @@ -195,7 +195,7 @@ def test_get_per_issue_metrics_with_hide_envs(self): mock_issue1 = MagicMock( title="Issue 1", html_url="https://github.com/user/repo/issues/1", - author="alice", + user={"login": "alice"}, state="open", comments=1, created_at="2023-01-01T00:00:00Z", @@ -209,7 +209,7 @@ def test_get_per_issue_metrics_with_hide_envs(self): mock_issue2 = MagicMock( title="Issue 2", html_url="https://github.com/user/repo/issues/2", - author="bob", + user={"login": "bob"}, state="closed", comments=1, created_at="2023-01-01T00:00:00Z", @@ -304,7 +304,7 @@ def test_get_per_issue_metrics_without_hide_envs(self): mock_issue1 = MagicMock( title="Issue 1", html_url="https://github.com/user/repo/issues/1", - author="alice", + user={"login": "alice"}, state="open", comments=1, created_at="2023-01-01T00:00:00Z", @@ -318,7 +318,7 @@ def test_get_per_issue_metrics_without_hide_envs(self): mock_issue2 = MagicMock( title="Issue 2", html_url="https://github.com/user/repo/issues/2", - author="bob", + user={"login": "bob"}, state="closed", comments=1, created_at="2023-01-01T00:00:00Z", @@ -391,6 +391,94 @@ def test_get_per_issue_metrics_without_hide_envs(self): expected_issues_with_metrics[1].time_to_close, ) + @patch.dict( + os.environ, + { + "GH_TOKEN": "test_token", + "SEARCH_QUERY": "is:issue is:open repo:user/repo", + "IGNORE_USERS": "alice", + }, + ) + def test_get_per_issue_metrics_with_ignore_users(self): + """ + Test that the function correctly filters out issues with authors in the IGNORE_USERS variable + """ + + # Create mock data + mock_issue1 = MagicMock( + title="Issue 1", + html_url="https://github.com/user/repo/issues/1", + user={"login": "alice"}, + state="open", + comments=1, + created_at="2023-01-01T00:00:00Z", + ) + + mock_comment1 = MagicMock() + mock_comment1.created_at = datetime.fromisoformat("2023-01-02T00:00:00Z") + mock_issue1.issue.comments.return_value = [mock_comment1] + mock_issue1.issue.pull_request_urls = None + + mock_issue2 = MagicMock( + title="Issue 2", + html_url="https://github.com/user/repo/issues/2", + user={"login": "bob"}, + state="closed", + comments=1, + created_at="2023-01-01T00:00:00Z", + closed_at="2023-01-04T00:00:00Z", + ) + + mock_comment2 = MagicMock() + mock_comment2.created_at = datetime.fromisoformat("2023-01-03T00:00:00Z") + mock_issue2.issue.comments.return_value = [mock_comment2] + mock_issue2.issue.pull_request_urls = None + + issues = [ + mock_issue1, + mock_issue2, + ] + + # Call the function and check the result + with unittest.mock.patch( # type:ignore + "issue_metrics.measure_time_to_first_response", + measure_time_to_first_response, + ), unittest.mock.patch( # type:ignore + "issue_metrics.measure_time_to_close", measure_time_to_close + ): + ( + result_issues_with_metrics, + result_num_issues_open, + result_num_issues_closed, + ) = get_per_issue_metrics( + issues, + env_vars=get_env_vars(test=True), + ignore_users=["alice"], + ) + expected_issues_with_metrics = [ + IssueWithMetrics( + "Issue 2", + "https://github.com/user/repo/issues/2", + "bob", + timedelta(days=2), + timedelta(days=3), + None, + None, + ), + ] + expected_num_issues_open = 0 + expected_num_issues_closed = 1 + self.assertEqual(result_num_issues_open, expected_num_issues_open) + self.assertEqual(result_num_issues_closed, expected_num_issues_closed) + self.assertEqual( + result_issues_with_metrics[0].time_to_first_response, + expected_issues_with_metrics[0].time_to_first_response, + ) + self.assertEqual( + result_issues_with_metrics[0].time_to_close, + expected_issues_with_metrics[0].time_to_close, + ) + class TestDiscussionMetrics(unittest.TestCase): """Test suite for the discussion_metrics function.""" From 3407ecbf31e848495960ddba90dadc3939ee78b9 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Mon, 23 Sep 2024 21:53:59 -0700 Subject: [PATCH 142/358] chore: run prettier on README Signed-off-by: Zack Koppert --- README.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 6e92765d..575deda1 100644 --- a/README.md +++ b/README.md @@ -139,25 +139,25 @@ This action can be configured to authenticate with GitHub App Installation or Pe #### Other Configuration Options -| field | required | default | description | -| ----------------------------- | -------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `GH_ENTERPRISE_URL` | False | `""` | URL of GitHub Enterprise instance to use for auth instead of github.com | -| `HIDE_AUTHOR` | False | False | If set to `true`, the author will not be displayed in the generated Markdown file. | -| `HIDE_ITEMS_CLOSED_COUNT` | False | False | If set to `true`, the number of items closed metric will not be displayed in the generated Markdown file. | -| `HIDE_LABEL_METRICS` | False | False | If set to `true`, the time in label metrics will not be displayed in the generated Markdown file. | -| `HIDE_TIME_TO_ANSWER` | False | False | If set to `true`, the time to answer a discussion will not be displayed in the generated Markdown file. | -| `HIDE_TIME_TO_CLOSE` | False | False | If set to `true`, the time to close will not be displayed in the generated Markdown file. | -| `HIDE_TIME_TO_FIRST_RESPONSE` | False | False | If set to `true`, the time to first response will not be displayed in the generated Markdown file. | +| field | required | default | description | +| ----------------------------- | -------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `GH_ENTERPRISE_URL` | False | `""` | URL of GitHub Enterprise instance to use for auth instead of github.com | +| `HIDE_AUTHOR` | False | False | If set to `true`, the author will not be displayed in the generated Markdown file. | +| `HIDE_ITEMS_CLOSED_COUNT` | False | False | If set to `true`, the number of items closed metric will not be displayed in the generated Markdown file. | +| `HIDE_LABEL_METRICS` | False | False | If set to `true`, the time in label metrics will not be displayed in the generated Markdown file. | +| `HIDE_TIME_TO_ANSWER` | False | False | If set to `true`, the time to answer a discussion will not be displayed in the generated Markdown file. | +| `HIDE_TIME_TO_CLOSE` | False | False | If set to `true`, the time to close will not be displayed in the generated Markdown file. | +| `HIDE_TIME_TO_FIRST_RESPONSE` | False | False | If set to `true`, the time to first response will not be displayed in the generated Markdown file. | | `IGNORE_USERS` | False | False | A comma separated list of users to ignore when calculating metrics. (ie. `IGNORE_USERS: 'user1,user2'`). To ignore bots, append `[bot]` to the user (ie. `IGNORE_USERS: 'github-actions[bot]'`) Users in this list will also have their authored issues and pull requests removed from the markdown table. | -| `ENABLE_MENTOR_COUNT` | False | False | If set to 'TRUE' count number of comments users left on discussions, issues and PRs and display number of active mentors | -| `MIN_MENTOR_COMMENTS` | False | 10 | Minimum number of comments to count as a mentor | -| `MAX_COMMENTS_EVAL` | False | 20 | Maximum number of comments per thread to evaluate for mentor stats | -| `HEAVILY_INVOLVED_CUTOFF` | False | 3 | Cutoff after which a mentor's comments in one issue are no longer counted against their total score | -| `LABELS_TO_MEASURE` | False | `""` | A comma separated list of labels to measure how much time the label is applied. If not provided, no labels durations will be measured. Not compatible with discussions at this time. | -| `NON_MENTIONING_LINKS` | False | False | If set to `true`, will use non-mentioning GitHub links to avoid linking to the generated issue from the source repository. Links of the form `https://www.github.com` will be used. | -| `OUTPUT_FILE` | False | `issue_metrics.md` or `issue_metrics.json` | Output filename. | -| `REPORT_TITLE` | False | `"Issue Metrics"` | Title to have on the report issue. | -| `SEARCH_QUERY` | True | `""` | The query by which you can filter issues/PRs which must contain a `repo:`, `org:`, `owner:`, or a `user:` entry. For discussions, include `type:discussions` in the query. | +| `ENABLE_MENTOR_COUNT` | False | False | If set to 'TRUE' count number of comments users left on discussions, issues and PRs and display number of active mentors | +| `MIN_MENTOR_COMMENTS` | False | 10 | Minimum number of comments to count as a mentor | +| `MAX_COMMENTS_EVAL` | False | 20 | Maximum number of comments per thread to evaluate for mentor stats | +| `HEAVILY_INVOLVED_CUTOFF` | False | 3 | Cutoff after which a mentor's comments in one issue are no longer counted against their total score | +| `LABELS_TO_MEASURE` | False | `""` | A comma separated list of labels to measure how much time the label is applied. If not provided, no labels durations will be measured. Not compatible with discussions at this time. | +| `NON_MENTIONING_LINKS` | False | False | If set to `true`, will use non-mentioning GitHub links to avoid linking to the generated issue from the source repository. Links of the form `https://www.github.com` will be used. | +| `OUTPUT_FILE` | False | `issue_metrics.md` or `issue_metrics.json` | Output filename. | +| `REPORT_TITLE` | False | `"Issue Metrics"` | Title to have on the report issue. | +| `SEARCH_QUERY` | True | `""` | The query by which you can filter issues/PRs which must contain a `repo:`, `org:`, `owner:`, or a `user:` entry. For discussions, include `type:discussions` in the query. | ## Further Documentation From 04a9ee26a4e13deff47ecf9e6f43b5d41ae177a1 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Mon, 23 Sep 2024 21:54:52 -0700 Subject: [PATCH 143/358] chore: proper spelling of Markdown Signed-off-by: Zack Koppert --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 575deda1..11f6f8bb 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ This action can be configured to authenticate with GitHub App Installation or Pe | `HIDE_TIME_TO_ANSWER` | False | False | If set to `true`, the time to answer a discussion will not be displayed in the generated Markdown file. | | `HIDE_TIME_TO_CLOSE` | False | False | If set to `true`, the time to close will not be displayed in the generated Markdown file. | | `HIDE_TIME_TO_FIRST_RESPONSE` | False | False | If set to `true`, the time to first response will not be displayed in the generated Markdown file. | -| `IGNORE_USERS` | False | False | A comma separated list of users to ignore when calculating metrics. (ie. `IGNORE_USERS: 'user1,user2'`). To ignore bots, append `[bot]` to the user (ie. `IGNORE_USERS: 'github-actions[bot]'`) Users in this list will also have their authored issues and pull requests removed from the markdown table. | +| `IGNORE_USERS` | False | False | A comma separated list of users to ignore when calculating metrics. (ie. `IGNORE_USERS: 'user1,user2'`). To ignore bots, append `[bot]` to the user (ie. `IGNORE_USERS: 'github-actions[bot]'`) Users in this list will also have their authored issues and pull requests removed from the Markdown table. | | `ENABLE_MENTOR_COUNT` | False | False | If set to 'TRUE' count number of comments users left on discussions, issues and PRs and display number of active mentors | | `MIN_MENTOR_COMMENTS` | False | 10 | Minimum number of comments to count as a mentor | | `MAX_COMMENTS_EVAL` | False | 20 | Maximum number of comments per thread to evaluate for mentor stats | From 37c12e76ed691371b968a02344edb22331135f2c Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 24 Sep 2024 09:46:51 -0700 Subject: [PATCH 144/358] fix: Remove more general part of duplicate actions permissions --- .github/workflows/major-version-updater.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/major-version-updater.yml b/.github/workflows/major-version-updater.yml index dafb6f27..50545283 100644 --- a/.github/workflows/major-version-updater.yml +++ b/.github/workflows/major-version-updater.yml @@ -5,9 +5,6 @@ on: release: types: [published] -permissions: - contents: write - jobs: update-major-version-tag: runs-on: ubuntu-latest From 53c6eba9280cd5dc181c2c58939f84f283602cbf Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 24 Sep 2024 09:52:53 -0700 Subject: [PATCH 145/358] Looks like if we don't specify permissions, it defaults to write --- .github/workflows/major-version-updater.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/major-version-updater.yml b/.github/workflows/major-version-updater.yml index 50545283..0da7a65b 100644 --- a/.github/workflows/major-version-updater.yml +++ b/.github/workflows/major-version-updater.yml @@ -5,6 +5,9 @@ on: release: types: [published] +permissions: + contents: read-all + jobs: update-major-version-tag: runs-on: ubuntu-latest From 85733416dee7aeae725e37b7055be5affd808c73 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 24 Sep 2024 09:55:58 -0700 Subject: [PATCH 146/358] following the docs rather than the security guidance --- .github/workflows/major-version-updater.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/major-version-updater.yml b/.github/workflows/major-version-updater.yml index 0da7a65b..0d242c85 100644 --- a/.github/workflows/major-version-updater.yml +++ b/.github/workflows/major-version-updater.yml @@ -6,7 +6,7 @@ on: types: [published] permissions: - contents: read-all + contents: read jobs: update-major-version-tag: From e3d639ac36dcae0d22fd862a2dedd87a3b987bc7 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 27 Sep 2024 13:21:21 -0700 Subject: [PATCH 147/358] docs(setup): developer setup instructions to run codebase --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 11f6f8bb..9717bef8 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,20 @@ This action can be configured to authenticate with GitHub App Installation or Pe We would ❤️ contributions to improve this action. Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for how to get involved. +### Development Setup +Ensure you have python `3.10+` installed +Clone this repo and cd into `issue-metrics` +Create python virtual env + `python3 -m venv .venv` +Activate virtual env + `source .venv/bin/activate` +Install dependencies + `pip install -r requirements.txt -r requirements-test.txt` +Run tests + `make test` +Run linter + `make lint` + ## License [MIT](LICENSE) From 4735df79d1b93c9212f708c819deb98056e74fae Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 27 Sep 2024 13:44:04 -0700 Subject: [PATCH 148/358] docs(setup): Prettier updates --- README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9717bef8..d8e6221e 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ name: Monthly issue metrics on: workflow_dispatch: schedule: - - cron: "3 2 1 * *" + - cron: '3 2 1 * *' permissions: contents: read @@ -175,17 +175,18 @@ This action can be configured to authenticate with GitHub App Installation or Pe We would ❤️ contributions to improve this action. Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for how to get involved. ### Development Setup -Ensure you have python `3.10+` installed -Clone this repo and cd into `issue-metrics` -Create python virtual env + +- Ensure you have python `3.10+` installed +- Clone this repo and cd into `issue-metrics` +- Create python virtual env `python3 -m venv .venv` -Activate virtual env +- Activate virtual env `source .venv/bin/activate` -Install dependencies +- Install dependencies `pip install -r requirements.txt -r requirements-test.txt` -Run tests +- Run tests `make test` -Run linter +- Run linter `make lint` ## License From a6eef8c8ba1a0efe14c16cd5321b870b16c95d06 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 27 Sep 2024 13:44:39 -0700 Subject: [PATCH 149/358] docs(setup): Language update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d8e6221e..17898c75 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ We would ❤️ contributions to improve this action. Please see [CONTRIBUTING.m ### Development Setup - Ensure you have python `3.10+` installed -- Clone this repo and cd into `issue-metrics` +- Clone this repository and cd into `issue-metrics` - Create python virtual env `python3 -m venv .venv` - Activate virtual env From 9a6421fca09c48718c9c88954bc20eafc6b98e59 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 27 Sep 2024 13:49:44 -0700 Subject: [PATCH 150/358] docs(setup): Prettier update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 17898c75..6db80bc5 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ name: Monthly issue metrics on: workflow_dispatch: schedule: - - cron: '3 2 1 * *' + - cron: "3 2 1 * *" permissions: contents: read From d4461a55da8ef231d6a0759bcbff1cb980bfbdda Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 27 Sep 2024 16:17:44 -0700 Subject: [PATCH 151/358] docs: instructions on verifying token access to a repository --- README.md | 3 +- docs/verify-token-access-to-repository.md | 65 +++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 docs/verify-token-access-to-repository.md diff --git a/README.md b/README.md index 11f6f8bb..02c42b35 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ name: Monthly issue metrics on: workflow_dispatch: schedule: - - cron: "3 2 1 * *" + - cron: '3 2 1 * *' permissions: contents: read @@ -110,6 +110,7 @@ All feedback regarding our GitHub Actions, as a whole, should be communicated th - Do this by creating a [GitHub API token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic) with permissions to read the repository and write issues. - Then take the value of the API token you just created, and [create a repository secret](https://docs.github.com/en/actions/security-guides/encrypted-secrets) where the name of the secret is `GH_TOKEN` and the value of the secret the API token. - Then finally update the workflow file to use that repository secret by changing `GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}` to `GH_TOKEN: ${{ secrets.GH_TOKEN }}`. The name of the secret can really be anything. It just needs to match between when you create the secret name and when you refer to it in the workflow file. + - Help on verifying your token's access to your repository [here](docs/verify-token-access-to-repository.md) 6. If you want the resulting issue with the metrics in it to appear in a different repository other than the one the workflow file runs in, update the line `token: ${{ secrets.GITHUB_TOKEN }}` with your own GitHub API token stored as a repository secret. - This process is the same as described in the step above. More info on creating secrets can be found [here](https://docs.github.com/en/actions/security-guides/encrypted-secrets). 7. Commit the workflow file to the default branch (often `master` or `main`) diff --git a/docs/verify-token-access-to-repository.md b/docs/verify-token-access-to-repository.md new file mode 100644 index 00000000..3b0c5fe9 --- /dev/null +++ b/docs/verify-token-access-to-repository.md @@ -0,0 +1,65 @@ +## Verify Token Access to Repository + +Github PAT token access can be confusing. Here's a quick way to test if the token you're using is authorized to access your repository. + +**Remove this snippet after you've verified your token.** + +- Make sure you follow the token setup instructions [here](https://github.com/github/issue-metrics/tree/main?tab=readme-ov-file#use-as-a-github-action) first. + +- Replace `{owner/repo}` with your own repo information. + +- Add this snippet to your workflow.yml. + +``` +- name: Check GitHub token permissions + run: | + curl -H "Authorization: token ${{ secrets.GH_TOKEN }}" https://api.github.com/repos/{owner/repo} +``` + +- Go to your repository Actions in Github and run your job. +- In the job run details, click into the results of `Check Github token permissions` +- You should see your token details with no errors. + +Example of the snippet in the full workflow: + +``` +name: Monthly issue metrics +on: + workflow_dispatch: + schedule: + - cron: '3 2 1 * *' + +permissions: + contents: read + +jobs: + build: + name: issue metrics + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: read + + steps: + - name: Check GitHub token permissions + run: | + curl -H "Authorization: token ${{ secrets.GH_TOKEN }}" https://api.github.com/{owner/repo} + - name: Get dates for last month + shell: bash + run: | + # Calculate the first day of the previous month + first_day=$(date -d "last month" +%Y-%m-01) + + # Calculate the last day of the previous month + last_day=$(date -d "$first_day +1 month -1 day" +%Y-%m-%d) + + #Set an environment variable with the date range + echo "$first_day..$last_day" + echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV" + + - name: Run issue-metrics tool + uses: github/issue-metrics@v3 + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + SEARCH_QUERY: 'repo:{owner/repo} is:issue created:${{ env.last_month }}' +``` From f4ae9f177fe38aad012affb814426f42b65cea99 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 27 Sep 2024 16:24:31 -0700 Subject: [PATCH 152/358] docs: prettier updates --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 02c42b35..a88e47be 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ name: Monthly issue metrics on: workflow_dispatch: schedule: - - cron: '3 2 1 * *' + - cron: "3 2 1 * *" permissions: contents: read From c32ea51204c14a813a12a6268284ba992c4d5814 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 27 Sep 2024 16:26:03 -0700 Subject: [PATCH 153/358] docs: language update --- docs/verify-token-access-to-repository.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/verify-token-access-to-repository.md b/docs/verify-token-access-to-repository.md index 3b0c5fe9..d69a41bf 100644 --- a/docs/verify-token-access-to-repository.md +++ b/docs/verify-token-access-to-repository.md @@ -6,7 +6,7 @@ Github PAT token access can be confusing. Here's a quick way to test if the toke - Make sure you follow the token setup instructions [here](https://github.com/github/issue-metrics/tree/main?tab=readme-ov-file#use-as-a-github-action) first. -- Replace `{owner/repo}` with your own repo information. +- Replace `{owner/repo}` with your own repository information. - Add this snippet to your workflow.yml. From 57de64ac248dc1de8daa8bf46bcc0eb55ca72276 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 27 Sep 2024 16:30:37 -0700 Subject: [PATCH 154/358] docs: correct more linting CI errors --- docs/verify-token-access-to-repository.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/verify-token-access-to-repository.md b/docs/verify-token-access-to-repository.md index d69a41bf..b5ccc13b 100644 --- a/docs/verify-token-access-to-repository.md +++ b/docs/verify-token-access-to-repository.md @@ -1,4 +1,4 @@ -## Verify Token Access to Repository +# Verify Token Access to Repository Github PAT token access can be confusing. Here's a quick way to test if the token you're using is authorized to access your repository. @@ -10,19 +10,19 @@ Github PAT token access can be confusing. Here's a quick way to test if the toke - Add this snippet to your workflow.yml. -``` +```yml - name: Check GitHub token permissions run: | curl -H "Authorization: token ${{ secrets.GH_TOKEN }}" https://api.github.com/repos/{owner/repo} ``` - Go to your repository Actions in Github and run your job. -- In the job run details, click into the results of `Check Github token permissions` +- In the job run details, click into the results of `Check GitHub token permissions` - You should see your token details with no errors. Example of the snippet in the full workflow: -``` +```yml name: Monthly issue metrics on: workflow_dispatch: From 2070ad283720c3f4d7d80a449aae3caaa2424831 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 27 Sep 2024 16:34:44 -0700 Subject: [PATCH 155/358] docs: more lint fixes --- docs/verify-token-access-to-repository.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/verify-token-access-to-repository.md b/docs/verify-token-access-to-repository.md index b5ccc13b..25a4f1be 100644 --- a/docs/verify-token-access-to-repository.md +++ b/docs/verify-token-access-to-repository.md @@ -1,6 +1,6 @@ # Verify Token Access to Repository -Github PAT token access can be confusing. Here's a quick way to test if the token you're using is authorized to access your repository. +GitHub PAT token access can be confusing. Here's a quick way to test if the token you're using is authorized to access your repository. **Remove this snippet after you've verified your token.** @@ -16,7 +16,7 @@ Github PAT token access can be confusing. Here's a quick way to test if the toke curl -H "Authorization: token ${{ secrets.GH_TOKEN }}" https://api.github.com/repos/{owner/repo} ``` -- Go to your repository Actions in Github and run your job. +- Go to your repository Actions in GitHub and run your job. - In the job run details, click into the results of `Check GitHub token permissions` - You should see your token details with no errors. @@ -27,7 +27,7 @@ name: Monthly issue metrics on: workflow_dispatch: schedule: - - cron: '3 2 1 * *' + - cron: "3 2 1 * *" permissions: contents: read @@ -61,5 +61,5 @@ jobs: uses: github/issue-metrics@v3 env: GH_TOKEN: ${{ secrets.GH_TOKEN }} - SEARCH_QUERY: 'repo:{owner/repo} is:issue created:${{ env.last_month }}' + SEARCH_QUERY: "repo:{owner/repo} is:issue created:${{ env.last_month }}" ``` From dbf75d080a738f978cfa176d824a88365facc0bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 17:16:23 +0000 Subject: [PATCH 156/358] chore(deps): bump python from `15bad98` to `ad48727` Bumps python from `15bad98` to `ad48727`. --- updated-dependencies: - dependency-name: python dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index b8be812a..7c13501b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.12-slim@sha256:15bad989b293be1dd5eb26a87ecacadaee1559f98e29f02bf6d00c8d86129f39 +FROM python:3.12-slim@sha256:ad48727987b259854d52241fac3bc633574364867b8e20aec305e6e7f4028b26 LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From cd95286fd024f7a257a7cd8ee3cdd1bd49d3d6bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 17:51:10 +0000 Subject: [PATCH 157/358] chore(deps): bump the dependencies group with 4 updates Bumps the dependencies group with 4 updates: [peter-evans/create-issue-from-file](https://github.com/peter-evans/create-issue-from-file), [actions/checkout](https://github.com/actions/checkout), [docker/build-push-action](https://github.com/docker/build-push-action) and [github/codeql-action](https://github.com/github/codeql-action). Updates `peter-evans/create-issue-from-file` from 5.0.0 to 5.0.1 - [Release notes](https://github.com/peter-evans/create-issue-from-file/releases) - [Commits](https://github.com/peter-evans/create-issue-from-file/compare/24452a72d85239eacf1468b0f1982a9f3fec4c94...e8ef132d6df98ed982188e460ebb3b5d4ef3a9cd) Updates `actions/checkout` from 4.1.7 to 4.2.0 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/692973e3d937129bcbf40652eb9f2f61becf3332...d632683dd7b4114ad314bca15554477dd762a938) Updates `docker/build-push-action` from 6.7.0 to 6.9.0 - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/5cd11c3a4ced054e52742c5fd54dca954e0edd85...4f58ea79222b3b9dc2c8bbdd6debcef730109a75) Updates `github/codeql-action` from 3.26.8 to 3.26.10 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/294a9d92911152fe08befb9ec03e240add280cb3...e2b3eafc8d227b0241d48be5f425d47c2d750a13) --- updated-dependencies: - dependency-name: peter-evans/create-issue-from-file dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/contributor_report.yaml | 2 +- .github/workflows/docker-image.yml | 2 +- .github/workflows/linter.yaml | 2 +- .github/workflows/major-version-updater.yml | 2 +- .github/workflows/python-package.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/scorecard.yml | 4 ++-- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/contributor_report.yaml b/.github/workflows/contributor_report.yaml index 62ef6141..5ffdb749 100644 --- a/.github/workflows/contributor_report.yaml +++ b/.github/workflows/contributor_report.yaml @@ -36,7 +36,7 @@ jobs: SPONSOR_INFO: "true" - name: Create issue - uses: peter-evans/create-issue-from-file@24452a72d85239eacf1468b0f1982a9f3fec4c94 + uses: peter-evans/create-issue-from-file@e8ef132d6df98ed982188e460ebb3b5d4ef3a9cd with: title: Monthly contributor report token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 0af83305..30bb4bae 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -14,6 +14,6 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Build the Docker image run: docker build . --file Dockerfile --platform linux/amd64 --tag issue-metrics:"$(date +%s)" diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index ecbc5c80..df37790a 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -18,7 +18,7 @@ jobs: statuses: write steps: - name: Checkout Code - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: # Full git history is needed to get a proper # list of changed files within `super-linter` diff --git a/.github/workflows/major-version-updater.yml b/.github/workflows/major-version-updater.yml index 0d242c85..26e4053c 100644 --- a/.github/workflows/major-version-updater.yml +++ b/.github/workflows/major-version-updater.yml @@ -15,7 +15,7 @@ jobs: contents: write steps: - name: Checkout Repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: version id: version diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index f56a0dc9..aa38f4fa 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -21,7 +21,7 @@ jobs: python-version: [3.11, 3.12] steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ecac2b9a..9df473c8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,10 +60,10 @@ jobs: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Push Docker Image if: ${{ success() }} - uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 + uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 with: context: . file: ./Dockerfile diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index ea50b855..23608870 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -25,7 +25,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: persist-credentials: false @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@294a9d92911152fe08befb9ec03e240add280cb3 # v3.24.9 + uses: github/codeql-action/upload-sarif@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.24.9 with: sarif_file: results.sarif From c151e4678a8bda34d817a6b90a4919877561dd31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 18:00:06 +0000 Subject: [PATCH 158/358] chore(deps): bump pylint from 3.3.0 to 3.3.1 in the dependencies group Bumps the dependencies group with 1 update: [pylint](https://github.com/pylint-dev/pylint). Updates `pylint` from 3.3.0 to 3.3.1 - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v3.3.0...v3.3.1) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 15f17832..fdbfa048 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,7 +2,7 @@ black==24.8.0 flake8==7.1.1 mypy==1.11.2 mypy-extensions==1.0.0 -pylint==3.3.0 +pylint==3.3.1 pytest==8.3.3 pytest-cov==5.0.0 types-pytz==2024.2.0.20240913 From 0a73c50ba9435ff644722223746b0ae43e77fa83 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Mon, 30 Sep 2024 11:29:46 -0700 Subject: [PATCH 159/358] Chore: use setup python to improve OSSF scorecard --- .github/workflows/linter.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index df37790a..b72e6925 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -23,12 +23,14 @@ jobs: # Full git history is needed to get a proper # list of changed files within `super-linter` fetch-depth: 0 + - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + with: + python-version: '3.12' - name: Install dependencies run: | - python -m pip install --upgrade pip pip install -r requirements.txt -r requirements-test.txt - name: Lint Code Base - uses: super-linter/super-linter@b92721f792f381cedc002ecdbb9847a15ece5bb8 + uses: super-linter/super-linter@b92721f792f381cedc002ecdbb9847a15ece5bb8 # v7.1.0 env: DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 47ddf68524bbf1afa6b97c8f4d24c288171b5e9a Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Mon, 30 Sep 2024 11:48:12 -0700 Subject: [PATCH 160/358] Add OSSF Best Practices Badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 0339828d..11fbde62 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![Docker Image CI](https://github.com/github/issue-metrics/actions/workflows/docker-image.yml/badge.svg)](https://github.com/github/issue-metrics/actions/workflows/docker-image.yml) [![Python package](https://github.com/github/issue-metrics/actions/workflows/python-package.yml/badge.svg)](https://github.com/github/issue-metrics/actions/workflows/python-package.yml) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/github/issue-metrics/badge)](https://scorecard.dev/viewer/?uri=github.com/github/issue-metrics) +[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/9501/badge)](https://www.bestpractices.dev/projects/9501) This is a GitHub Action that searches for issues/pull requests/discussions in a repository, measures several metrics, and generates a report in form of a GitHub issue. The issues/pull requests/discussions to search for can be filtered by using a search query. From d51517fb549584adadc901795886f9ab63589883 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Mon, 30 Sep 2024 11:57:32 -0700 Subject: [PATCH 161/358] chore: run prettier Signed-off-by: Zack Koppert --- .github/workflows/linter.yaml | 2 +- config.py | 24 ++++++++++++++---------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index b72e6925..1015481e 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -25,7 +25,7 @@ jobs: fetch-depth: 0 - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 with: - python-version: '3.12' + python-version: "3.12" - name: Install dependencies run: | pip install -r requirements.txt -r requirements-test.txt diff --git a/config.py b/config.py index b45093a8..7f22ac69 100644 --- a/config.py +++ b/config.py @@ -22,11 +22,13 @@ class EnvVars: Environment variables Attributes: + enable_mentor_count (bool): If set to TRUE, compute number of mentors gh_app_id (int | None): The GitHub App ID to use for authentication gh_app_installation_id (int | None): The GitHub App Installation ID to use for authentication gh_app_private_key_bytes (bytes): The GitHub App Private Key as bytes to use for authentication gh_token (str | None): GitHub personal access token (PAT) for API authentication ghe (str): The GitHub Enterprise URL to use for authentication + heavily_involved_cutoff (str): If set, defines the cutoff after which heavily involved commentors in hide_author (bool): If true, the author's information is hidden in the output hide_items_closed_count (bool): If true, the number of items closed metric is hidden in the output hide_label_metrics (bool): If true, the label metrics are hidden in the output @@ -35,23 +37,25 @@ class EnvVars: hide_time_to_first_response (bool): If true, the time to first response metric is hidden in the output ignore_users (List[str]): List of usernames to ignore when calculating metrics labels_to_measure (List[str]): List of labels to measure how much time the lable is applied - enable_mentor_count (bool): If set to TRUE, compute number of mentors - min_mentor_comments (str): If set, defines the minimum number of comments for mentors max_comments_eval (str): If set, defines the maximum number of comments to look at for mentor evaluation - heavily_involved_cutoff (str): If set, defines the cutoff after which heavily involved commentors in - search_query (str): Search query used to filter issues/prs/discussions on GitHub + min_mentor_comments (str): If set, defines the minimum number of comments for mentors non_mentioning_links (bool): If set to TRUE, links do not cause a notification in the desitnation repository - report_title (str): The title of the report output_file (str): The name of the file to write the report to + report_title (str): The title of the report + search_query (str): Search query used to filter issues/prs/discussions on GitHub + sort_by_author (bool): If true, the report will be sorted by author + sort_by_time_to_close (bool): If true, the report will be sorted by time to close """ def __init__( self, + enable_mentor_count: bool, gh_app_id: int | None, gh_app_installation_id: int | None, gh_app_private_key_bytes: bytes, gh_token: str | None, ghe: str | None, + heavily_involved_cutoff: str, hide_author: bool, hide_items_closed_count: bool, hide_label_metrics: bool, @@ -60,14 +64,14 @@ def __init__( hide_time_to_first_response: bool, ignore_user: List[str], labels_to_measure: List[str], - enable_mentor_count: bool, - min_mentor_comments: str, max_comments_eval: str, - heavily_involved_cutoff: str, - search_query: str, + min_mentor_comments: str, non_mentioning_links: bool, - report_title: str, output_file: str, + report_title: str, + search_query: str, + sort_by_author: bool, + sort_by_time_to_close: bool, ): self.gh_app_id = gh_app_id self.gh_app_installation_id = gh_app_installation_id From 93590cae4d788d28fc8db9f1f971b5f0fd83a3d2 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Mon, 30 Sep 2024 12:00:56 -0700 Subject: [PATCH 162/358] revert back to unalphabetized params --- config.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/config.py b/config.py index 7f22ac69..b43ea2be 100644 --- a/config.py +++ b/config.py @@ -22,13 +22,11 @@ class EnvVars: Environment variables Attributes: - enable_mentor_count (bool): If set to TRUE, compute number of mentors gh_app_id (int | None): The GitHub App ID to use for authentication gh_app_installation_id (int | None): The GitHub App Installation ID to use for authentication gh_app_private_key_bytes (bytes): The GitHub App Private Key as bytes to use for authentication gh_token (str | None): GitHub personal access token (PAT) for API authentication ghe (str): The GitHub Enterprise URL to use for authentication - heavily_involved_cutoff (str): If set, defines the cutoff after which heavily involved commentors in hide_author (bool): If true, the author's information is hidden in the output hide_items_closed_count (bool): If true, the number of items closed metric is hidden in the output hide_label_metrics (bool): If true, the label metrics are hidden in the output @@ -37,25 +35,23 @@ class EnvVars: hide_time_to_first_response (bool): If true, the time to first response metric is hidden in the output ignore_users (List[str]): List of usernames to ignore when calculating metrics labels_to_measure (List[str]): List of labels to measure how much time the lable is applied - max_comments_eval (str): If set, defines the maximum number of comments to look at for mentor evaluation + enable_mentor_count (bool): If set to TRUE, compute number of mentors min_mentor_comments (str): If set, defines the minimum number of comments for mentors + max_comments_eval (str): If set, defines the maximum number of comments to look at for mentor evaluation + heavily_involved_cutoff (str): If set, defines the cutoff after which heavily involved commentors in + search_query (str): Search query used to filter issues/prs/discussions on GitHub non_mentioning_links (bool): If set to TRUE, links do not cause a notification in the desitnation repository - output_file (str): The name of the file to write the report to report_title (str): The title of the report - search_query (str): Search query used to filter issues/prs/discussions on GitHub - sort_by_author (bool): If true, the report will be sorted by author - sort_by_time_to_close (bool): If true, the report will be sorted by time to close + output_file (str): The name of the file to write the report to """ def __init__( self, - enable_mentor_count: bool, gh_app_id: int | None, gh_app_installation_id: int | None, gh_app_private_key_bytes: bytes, gh_token: str | None, ghe: str | None, - heavily_involved_cutoff: str, hide_author: bool, hide_items_closed_count: bool, hide_label_metrics: bool, @@ -64,14 +60,14 @@ def __init__( hide_time_to_first_response: bool, ignore_user: List[str], labels_to_measure: List[str], - max_comments_eval: str, + enable_mentor_count: bool, min_mentor_comments: str, + max_comments_eval: str, + heavily_involved_cutoff: str, + search_query: str, non_mentioning_links: bool, - output_file: str, report_title: str, - search_query: str, - sort_by_author: bool, - sort_by_time_to_close: bool, + output_file: str, ): self.gh_app_id = gh_app_id self.gh_app_installation_id = gh_app_installation_id @@ -239,3 +235,4 @@ def get_env_vars(test: bool = False) -> EnvVars: report_title, output_file, ) + From d58e46dd88d1dd2dbe6215b90d30be21bf0f3705 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Mon, 30 Sep 2024 12:08:37 -0700 Subject: [PATCH 163/358] remove extra line return --- config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config.py b/config.py index b43ea2be..7cb753d0 100644 --- a/config.py +++ b/config.py @@ -234,5 +234,4 @@ def get_env_vars(test: bool = False) -> EnvVars: non_mentioning_links, report_title, output_file, - ) - + ) From ca5596f9c83a26425de97fe6cc0468aa30e5e511 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Mon, 30 Sep 2024 12:09:35 -0700 Subject: [PATCH 164/358] chore: remove end of line whitespace --- config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.py b/config.py index 7cb753d0..b45093a8 100644 --- a/config.py +++ b/config.py @@ -234,4 +234,4 @@ def get_env_vars(test: bool = False) -> EnvVars: non_mentioning_links, report_title, output_file, - ) + ) From 628a052d3a37c82b711910bb50873f071646ebdd Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Mon, 30 Sep 2024 15:27:37 -0700 Subject: [PATCH 165/358] Remove unneeded pip install python-setup takes car of the pip install and upgrade. This should also improve our OSSF score --- .github/workflows/python-package.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index aa38f4fa..f78b8b3e 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -23,12 +23,11 @@ jobs: steps: - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip pip install -r requirements.txt -r requirements-test.txt - name: Lint with flake8 and pylint run: | From e64e4e77e265909f99350662ab7237e7d4398c9a Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Fri, 4 Oct 2024 23:47:37 -0700 Subject: [PATCH 166/358] feat: add bypass to rate limiter Signed-off-by: Zack Koppert --- README.md | 1 + config.py | 6 ++++++ issue_metrics.py | 20 +++++++++++++++----- test_config.py | 3 +++ test_issue_metrics.py | 26 ++++++++++++++++++++++++++ 5 files changed, 51 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 11fbde62..8ea0c3dc 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,7 @@ This action can be configured to authenticate with GitHub App Installation or Pe | field | required | default | description | | ----------------------------- | -------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `GH_ENTERPRISE_URL` | False | `""` | URL of GitHub Enterprise instance to use for auth instead of github.com | +| `RATE_LIMIT_BYPASS` | False | `false` | If set to `true`, the rate limit will be bypassed. This is useful if being run on an local github server with rate limiting disabled. | | `HIDE_AUTHOR` | False | False | If set to `true`, the author will not be displayed in the generated Markdown file. | | `HIDE_ITEMS_CLOSED_COUNT` | False | False | If set to `true`, the number of items closed metric will not be displayed in the generated Markdown file. | | `HIDE_LABEL_METRICS` | False | False | If set to `true`, the time in label metrics will not be displayed in the generated Markdown file. | diff --git a/config.py b/config.py index b45093a8..4d544e4b 100644 --- a/config.py +++ b/config.py @@ -43,6 +43,7 @@ class EnvVars: non_mentioning_links (bool): If set to TRUE, links do not cause a notification in the desitnation repository report_title (str): The title of the report output_file (str): The name of the file to write the report to + rate_limit_bypass (bool): If set to TRUE, bypass the rate limit for the GitHub API """ def __init__( @@ -68,6 +69,7 @@ def __init__( non_mentioning_links: bool, report_title: str, output_file: str, + rate_limit_bypass: bool = False, ): self.gh_app_id = gh_app_id self.gh_app_installation_id = gh_app_installation_id @@ -90,6 +92,7 @@ def __init__( self.non_mentioning_links = non_mentioning_links self.report_title = report_title self.output_file = output_file + self.rate_limit_bypass = rate_limit_bypass def __repr__(self): return ( @@ -115,6 +118,7 @@ def __repr__(self): f"{self.non_mentioning_links}" f"{self.report_title}" f"{self.output_file}" + f"{self.rate_limit_bypass}" ) @@ -198,6 +202,7 @@ def get_env_vars(test: bool = False) -> EnvVars: report_title = os.getenv("REPORT_TITLE", "Issue Metrics") output_file = os.getenv("OUTPUT_FILE", "") + rate_limit_bypass = get_bool_env_var("RATE_LIMIT_BYPASS", False) # Hidden columns hide_author = get_bool_env_var("HIDE_AUTHOR", False) @@ -234,4 +239,5 @@ def get_env_vars(test: bool = False) -> EnvVars: non_mentioning_links, report_title, output_file, + rate_limit_bypass, ) diff --git a/issue_metrics.py b/issue_metrics.py index 3d176cfd..c50b09d2 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -50,6 +50,7 @@ def search_issues( search_query: str, github_connection: github3.GitHub, owners_and_repositories: List[dict], + rate_limit_bypass: bool = False, ) -> List[github3.search.IssueSearchResult]: # type: ignore """ Searches for issues/prs/discussions in a GitHub repository that match @@ -66,7 +67,13 @@ def search_issues( """ # Rate Limit Handling: API only allows 30 requests per minute - def wait_for_api_refresh(iterator: github3.structs.SearchIterator): + def wait_for_api_refresh( + iterator: github3.structs.SearchIterator, rate_limit_bypass: bool = False + ): + # If the rate limit bypass is enabled, don't wait for the API to refresh + if rate_limit_bypass: + return + max_retries = 5 retry_count = 0 sleep_time = 70 @@ -90,7 +97,7 @@ def wait_for_api_refresh(iterator: github3.structs.SearchIterator): issues_iterator = github_connection.search_issues( search_query, per_page=issues_per_page ) - wait_for_api_refresh(issues_iterator) + wait_for_api_refresh(issues_iterator, rate_limit_bypass) issues = [] repos_and_owners_string = "" @@ -107,7 +114,7 @@ def wait_for_api_refresh(iterator: github3.structs.SearchIterator): # requests are sent once per page of issues if idx % issues_per_page == 0: - wait_for_api_refresh(issues_iterator) + wait_for_api_refresh(issues_iterator, rate_limit_bypass) except github3.exceptions.ForbiddenError: print( @@ -288,7 +295,7 @@ def get_owners_and_repositories( return results_list -def main(): +def main(): # pragma: no cover """Run the issue-metrics script. This function authenticates to GitHub, searches for issues/prs/discussions @@ -312,6 +319,7 @@ def main(): non_mentioning_links = env_vars.non_mentioning_links report_title = env_vars.report_title output_file = env_vars.output_file + rate_limit_bypass = env_vars.rate_limit_bypass gh_app_id = env_vars.gh_app_id gh_app_installation_id = env_vars.gh_app_installation_id @@ -363,7 +371,9 @@ def main(): write_to_markdown(None, None, None, None, None, None, None, None) return else: - issues = search_issues(search_query, github_connection, owners_and_repositories) + issues = search_issues( + search_query, github_connection, owners_and_repositories, rate_limit_bypass + ) if len(issues) <= 0: print("No issues found") write_to_markdown(None, None, None, None, None, None, None, None) diff --git a/test_config.py b/test_config.py index 3a963154..ea7ddb28 100644 --- a/test_config.py +++ b/test_config.py @@ -83,6 +83,7 @@ def setUp(self): "OUTPUT_FILE", "REPORT_TITLE", "SEARCH_QUERY", + "RATE_LIMIT_BYPASS", ] for key in env_keys: if key in os.environ: @@ -108,6 +109,7 @@ def setUp(self): "OUTPUT_FILE": "", "REPORT_TITLE": "", "SEARCH_QUERY": SEARCH_QUERY, + "RATE_LIMIT_BYPASS": "false", }, clear=True, ) @@ -135,6 +137,7 @@ def test_get_env_vars_with_github_app(self): False, "", "", + False, ) result = get_env_vars(True) self.assertEqual(str(result), str(expected_result)) diff --git a/test_issue_metrics.py b/test_issue_metrics.py index 40e12445..027c1822 100644 --- a/test_issue_metrics.py +++ b/test_issue_metrics.py @@ -88,6 +88,32 @@ def test_search_issues_with_just_owner_or_org(self): issues = search_issues("is:open", mock_connection, owners) self.assertEqual(issues, mock_issues) + def test_search_issues_with_just_owner_or_org_with_bypass(self): + """Test that search_issues with just an owner/org returns the correct issues.""" + + # Set up the mock GitHub connection object + mock_issues = [ + MagicMock(title="Issue 1"), + MagicMock(title="Issue 2"), + MagicMock(title="Issue 3"), + ] + + # simulating github3.structs.SearchIterator return value + mock_search_result = MagicMock() + mock_search_result.__iter__.return_value = iter(mock_issues) + mock_search_result.ratelimit_remaining = 30 + + mock_connection = MagicMock() + mock_connection.search_issues.return_value = mock_search_result + + # Call search_issues and check that it returns the correct issues + org = {"owner": "org1"} + owners = [org] + issues = search_issues( + "is:open", mock_connection, owners, rate_limit_bypass=True + ) + self.assertEqual(issues, mock_issues) + class TestGetOwnerAndRepository(unittest.TestCase): """Unit tests for the get_owners_and_repositories function. From 364ff4444fc8a7676d0c42b30a10e3c5b942583f Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Fri, 4 Oct 2024 23:54:24 -0700 Subject: [PATCH 167/358] fix: correct github casing --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8ea0c3dc..d429178f 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ This action can be configured to authenticate with GitHub App Installation or Pe | field | required | default | description | | ----------------------------- | -------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `GH_ENTERPRISE_URL` | False | `""` | URL of GitHub Enterprise instance to use for auth instead of github.com | -| `RATE_LIMIT_BYPASS` | False | `false` | If set to `true`, the rate limit will be bypassed. This is useful if being run on an local github server with rate limiting disabled. | +| `RATE_LIMIT_BYPASS` | False | `false` | If set to `true`, the rate limit will be bypassed. This is useful if being run on an local GitHub server with rate limiting disabled. | | `HIDE_AUTHOR` | False | False | If set to `true`, the author will not be displayed in the generated Markdown file. | | `HIDE_ITEMS_CLOSED_COUNT` | False | False | If set to `true`, the number of items closed metric will not be displayed in the generated Markdown file. | | `HIDE_LABEL_METRICS` | False | False | If set to `true`, the time in label metrics will not be displayed in the generated Markdown file. | From 0f1bd2ba469f68d76afd95c9a3cf9ce3b332a5e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:09:42 +0000 Subject: [PATCH 168/358] chore(deps): bump numpy from 2.1.1 to 2.1.2 in the dependencies group Bumps the dependencies group with 1 update: [numpy](https://github.com/numpy/numpy). Updates `numpy` from 2.1.1 to 2.1.2 - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.1.1...v2.1.2) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b0a3b065..a99c6e0a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ github3.py==4.0.1 -numpy==2.1.1 +numpy==2.1.2 python-dotenv==1.0.1 pytz==2024.2 requests==2.32.3 From c85a84453172c50e45691a6e7b737b2dbf49b978 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:09:45 +0000 Subject: [PATCH 169/358] chore(deps): bump types-pytz from 2024.2.0.20240913 to 2024.2.0.20241003 Bumps [types-pytz](https://github.com/python/typeshed) from 2024.2.0.20240913 to 2024.2.0.20241003. - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-pytz dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index fdbfa048..db11884d 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -5,5 +5,5 @@ mypy-extensions==1.0.0 pylint==3.3.1 pytest==8.3.3 pytest-cov==5.0.0 -types-pytz==2024.2.0.20240913 +types-pytz==2024.2.0.20241003 types-requests==2.32.0.20240914 From 5e6689a9f1d32b754725e20f3ddc1c7d8bf58f05 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:21:53 +0000 Subject: [PATCH 170/358] chore(deps): bump python from `ad48727` to `af4e85f` Bumps python from `ad48727` to `af4e85f`. --- updated-dependencies: - dependency-name: python dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 7c13501b..3108a3d2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.12-slim@sha256:ad48727987b259854d52241fac3bc633574364867b8e20aec305e6e7f4028b26 +FROM python:3.12-slim@sha256:af4e85f1cac90dd3771e47292ea7c8a9830abfabbe4faa5c53f158854c2e819d LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From cabbc78975a1e94886cf92d371120c7fc768b9ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:43:53 +0000 Subject: [PATCH 171/358] chore(deps): bump the dependencies group with 4 updates Bumps the dependencies group with 4 updates: [actions/checkout](https://github.com/actions/checkout), [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action), [actions/upload-artifact](https://github.com/actions/upload-artifact) and [github/codeql-action](https://github.com/github/codeql-action). Updates `actions/checkout` from 4.2.0 to 4.2.1 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/d632683dd7b4114ad314bca15554477dd762a938...eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871) Updates `docker/setup-buildx-action` from 3.6.1 to 3.7.1 - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/988b5a0280414f521da01fcc63a27aeeb4b104db...c47758b77c9736f4b2ef4073d4d51994fabfe349) Updates `actions/upload-artifact` from 4.4.0 to 4.4.1 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/50769540e7f4bd5e21e526ee35c689e35e0d6874...604373da6381bf24206979c74d06a550515601b9) Updates `github/codeql-action` from 3.26.10 to 3.26.12 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/e2b3eafc8d227b0241d48be5f425d47c2d750a13...c36620d31ac7c881962c3d9dd939c40ec9434f2b) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: docker/setup-buildx-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/docker-image.yml | 2 +- .github/workflows/linter.yaml | 2 +- .github/workflows/major-version-updater.yml | 2 +- .github/workflows/python-package.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/scorecard.yml | 6 +++--- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 30bb4bae..a338d1b4 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -14,6 +14,6 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Build the Docker image run: docker build . --file Dockerfile --platform linux/amd64 --tag issue-metrics:"$(date +%s)" diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index 1015481e..bb49eefb 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -18,7 +18,7 @@ jobs: statuses: write steps: - name: Checkout Code - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: # Full git history is needed to get a proper # list of changed files within `super-linter` diff --git a/.github/workflows/major-version-updater.yml b/.github/workflows/major-version-updater.yml index 26e4053c..a4f5b5e4 100644 --- a/.github/workflows/major-version-updater.yml +++ b/.github/workflows/major-version-updater.yml @@ -15,7 +15,7 @@ jobs: contents: write steps: - name: Checkout Repo - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: version id: version diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index f78b8b3e..92f2fcd0 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -21,7 +21,7 @@ jobs: python-version: [3.11, 3.12] steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9df473c8..21442eb8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,14 +53,14 @@ jobs: IMAGE_NAME: github/issue_metrics # different than repo name (underscore instead of dash) steps: - name: Set up Docker Buildx - uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db + uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 - name: Log in to the Container registry uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Push Docker Image if: ${{ success() }} uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 23608870..4b673f77 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -25,7 +25,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: persist-credentials: false @@ -36,12 +36,12 @@ jobs: results_format: sarif publish_results: true - name: "Upload artifact" - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4.4.1 with: name: SARIF file path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.24.9 + uses: github/codeql-action/upload-sarif@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.24.9 with: sarif_file: results.sarif From 0adef4fce56982be506533188c5b64a3ce8e0368 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 17:18:28 +0000 Subject: [PATCH 172/358] chore(deps): bump the dependencies group with 2 updates Bumps the dependencies group with 2 updates: [black](https://github.com/psf/black) and [mypy](https://github.com/python/mypy). Updates `black` from 24.8.0 to 24.10.0 - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/compare/24.8.0...24.10.0) Updates `mypy` from 1.11.2 to 1.12.0 - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.11.2...v1.12.0) --- updated-dependencies: - dependency-name: black dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index db11884d..1d5ee6d0 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,6 +1,6 @@ -black==24.8.0 +black==24.10.0 flake8==7.1.1 -mypy==1.11.2 +mypy==1.12.0 mypy-extensions==1.0.0 pylint==3.3.1 pytest==8.3.3 From c025d4f56502e988e040a868c1b9569dd226af42 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 17:44:50 +0000 Subject: [PATCH 173/358] chore(deps): bump the dependencies group with 2 updates Bumps the dependencies group with 2 updates: [actions/upload-artifact](https://github.com/actions/upload-artifact) and [github/codeql-action](https://github.com/github/codeql-action). Updates `actions/upload-artifact` from 4.4.1 to 4.4.3 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/604373da6381bf24206979c74d06a550515601b9...b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882) Updates `github/codeql-action` from 3.26.12 to 3.26.13 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/c36620d31ac7c881962c3d9dd939c40ec9434f2b...f779452ac5af1c261dce0346a8f964149f49322b) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/scorecard.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 4b673f77..18dea43c 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -36,12 +36,12 @@ jobs: results_format: sarif publish_results: true - name: "Upload artifact" - uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4.4.1 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: SARIF file path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.24.9 + uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.24.9 with: sarif_file: results.sarif From 8d6b87eeed47c29752ceaadb8e7fdc0e393a4e3c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 17:49:43 +0000 Subject: [PATCH 174/358] chore(deps): bump python in the dependencies group Bumps the dependencies group with 1 update: python. Updates `python` from 3.12-slim to 3.13-slim --- updated-dependencies: - dependency-name: python dependency-type: direct:production dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 3108a3d2..434656f3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.12-slim@sha256:af4e85f1cac90dd3771e47292ea7c8a9830abfabbe4faa5c53f158854c2e819d +FROM python:3.13-slim@sha256:2ec5a4a5c3e919570f57675471f081d6299668d909feabd8d4803c6c61af666c LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From f957a282adefa87729322d188d7b8349ed3c1410 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 15 Oct 2024 17:28:44 -0700 Subject: [PATCH 175/358] fix: pass through report name, issue title, and search query when report is empty Signed-off-by: Zack Koppert --- issue_metrics.py | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index c50b09d2..eed55250 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -368,7 +368,23 @@ def main(): # pragma: no cover issues = get_discussions(token, search_query) if len(issues) <= 0: print("No discussions found") - write_to_markdown(None, None, None, None, None, None, None, None) + write_to_markdown( + issues_with_metrics=None, + average_time_to_first_response=None, + average_time_to_close=None, + average_time_to_answer=None, + average_time_in_labels=None, + num_issues_opened=None, + num_issues_closed=None, + num_mentor_count=None, + labels=None, + search_query=search_query, + hide_label_metrics=False, + hide_items_closed_count=False, + non_mentioning_links=False, + report_title=report_title, + output_file=output_file, + ) return else: issues = search_issues( @@ -376,7 +392,23 @@ def main(): # pragma: no cover ) if len(issues) <= 0: print("No issues found") - write_to_markdown(None, None, None, None, None, None, None, None) + write_to_markdown( + issues_with_metrics=None, + average_time_to_first_response=None, + average_time_to_close=None, + average_time_to_answer=None, + average_time_in_labels=None, + num_issues_opened=None, + num_issues_closed=None, + num_mentor_count=None, + labels=None, + search_query=search_query, + hide_label_metrics=False, + hide_items_closed_count=False, + non_mentioning_links=False, + report_title=report_title, + output_file=output_file, + ) return # Get all the metrics From 0405601ccc2906b0b2138120d70c0d922257e58a Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Wed, 16 Oct 2024 10:12:59 -0700 Subject: [PATCH 176/358] docs: update inline docs on function signature --- markdown_writer.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/markdown_writer.py b/markdown_writer.py index e9d8d3bf..98ec4a95 100644 --- a/markdown_writer.py +++ b/markdown_writer.py @@ -7,14 +7,21 @@ Functions: write_to_markdown( - issues_with_metrics: List[IssueWithMetrics], - average_time_to_first_response: timedelta, - average_time_to_close: timedelta, - average_time_to_answer: timedelta, - num_issues_opened: int, - num_issues_closed: int, - num_mentor_count: int, - file: file object = None + issues_with_metrics: Union[List[IssueWithMetrics], None], + average_time_to_first_response: Union[dict[str, timedelta], None], + average_time_to_close: Union[dict[str, timedelta], None], + average_time_to_answer: Union[dict[str, timedelta], None], + average_time_in_labels: Union[dict, None], + num_issues_opened: Union[int, None], + num_issues_closed: Union[int, None], + num_mentor_count: Union[int, None], + labels=None, + search_query=None, + hide_label_metrics=False, + hide_items_closed_count=False, + non_mentioning_links=False, + report_title="", + output_file="", ) -> None: Write the issues with metrics to a markdown file. get_non_hidden_columns( From 752d61ef366a2fd3d00bbec4a1975c5f62efcb89 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Wed, 16 Oct 2024 11:00:56 -0700 Subject: [PATCH 177/358] docs: fix docs to demonstrate type instead of defaults --- markdown_writer.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/markdown_writer.py b/markdown_writer.py index 98ec4a95..e8cb5e17 100644 --- a/markdown_writer.py +++ b/markdown_writer.py @@ -15,13 +15,13 @@ num_issues_opened: Union[int, None], num_issues_closed: Union[int, None], num_mentor_count: Union[int, None], - labels=None, - search_query=None, - hide_label_metrics=False, - hide_items_closed_count=False, - non_mentioning_links=False, - report_title="", - output_file="", + labels: List[str], + search_query: str, + hide_label_metrics: bool, + hide_items_closed_count: bool, + non_mentioning_links: bool, + report_title: str, + output_file: str, ) -> None: Write the issues with metrics to a markdown file. get_non_hidden_columns( From 747f7e04c6f04680a2fba50bfa456d3675620e1e Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Wed, 16 Oct 2024 14:18:25 -0700 Subject: [PATCH 178/358] docs: update function inline docs Signed-off-by: Zack Koppert --- test_issue_metrics.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test_issue_metrics.py b/test_issue_metrics.py index 027c1822..ab025af9 100644 --- a/test_issue_metrics.py +++ b/test_issue_metrics.py @@ -8,7 +8,6 @@ TestSearchIssues: A class to test the search_issues function. TestGetPerIssueMetrics: A class to test the get_per_issue_metrics function. TestGetEnvVars: A class to test the get_env_vars function. - TestMain: A class to test the main function. """ From 0e3c3f3bd1df0eeb1e785d56dc05a378f9b26849 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Fri, 18 Oct 2024 09:46:01 -0700 Subject: [PATCH 179/358] refactor: Move search functions and tests to their own files Signed-off-by: Zack Koppert --- issue_metrics.py | 132 +----------------------------------- search.py | 138 ++++++++++++++++++++++++++++++++++++++ test_issue_metrics.py | 144 +-------------------------------------- test_search.py | 152 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 293 insertions(+), 273 deletions(-) create mode 100644 search.py create mode 100644 test_search.py diff --git a/issue_metrics.py b/issue_metrics.py index eed55250..7a41f574 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -8,9 +8,6 @@ Functions: get_env_vars() -> EnvVars: Get the environment variables for use in the script. - search_issues(search_query: str, github_connection: github3.GitHub, owners_and_repositories: List[dict]) - -> github3.structs.SearchIterator: - Searches for issues in a GitHub repository that match the given search query. get_per_issue_metrics(issues: Union[List[dict], List[github3.issues.Issue]], discussions: bool = False), labels: Union[List[str], None] = None, ignore_users: List[str] = [] -> tuple[List, int, int]: @@ -21,8 +18,6 @@ """ import shutil -import sys -from time import sleep from typing import List, Union import github3 @@ -36,6 +31,7 @@ from markdown_helpers import markdown_too_large_for_issue_body, split_markdown_file from markdown_writer import write_to_markdown from most_active_mentors import count_comments_per_user, get_mentor_count +from search import get_owners_and_repositories, search_issues from time_to_answer import get_stats_time_to_answer, measure_time_to_answer from time_to_close import get_stats_time_to_close, measure_time_to_close from time_to_first_response import ( @@ -46,101 +42,6 @@ from time_to_ready_for_review import get_time_to_ready_for_review -def search_issues( - search_query: str, - github_connection: github3.GitHub, - owners_and_repositories: List[dict], - rate_limit_bypass: bool = False, -) -> List[github3.search.IssueSearchResult]: # type: ignore - """ - Searches for issues/prs/discussions in a GitHub repository that match - the given search query and handles errors related to GitHub API responses. - - Args: - search_query (str): The search query to use for finding issues/prs/discussions. - github_connection (github3.GitHub): A connection to the GitHub API. - owners_and_repositories (List[dict]): A list of dictionaries containing - the owner and repository names. - - Returns: - List[github3.search.IssueSearchResult]: A list of issues that match the search query. - """ - - # Rate Limit Handling: API only allows 30 requests per minute - def wait_for_api_refresh( - iterator: github3.structs.SearchIterator, rate_limit_bypass: bool = False - ): - # If the rate limit bypass is enabled, don't wait for the API to refresh - if rate_limit_bypass: - return - - max_retries = 5 - retry_count = 0 - sleep_time = 70 - - while iterator.ratelimit_remaining < 5: - if retry_count >= max_retries: - raise RuntimeError("Exceeded maximum retries for API rate limit") - - print( - f"GitHub API Rate Limit Low, waiting {sleep_time} seconds to refresh." - ) - sleep(sleep_time) - - # Exponentially increase the sleep time for the next retry - sleep_time *= 2 - retry_count += 1 - - issues_per_page = 100 - - print("Searching for issues...") - issues_iterator = github_connection.search_issues( - search_query, per_page=issues_per_page - ) - wait_for_api_refresh(issues_iterator, rate_limit_bypass) - - issues = [] - repos_and_owners_string = "" - for item in owners_and_repositories: - repos_and_owners_string += ( - f"{item.get('owner', '')}/{item.get('repository', '')} " - ) - - # Print the issue titles - try: - for idx, issue in enumerate(issues_iterator, 1): - print(issue.title) # type: ignore - issues.append(issue) - - # requests are sent once per page of issues - if idx % issues_per_page == 0: - wait_for_api_refresh(issues_iterator, rate_limit_bypass) - - except github3.exceptions.ForbiddenError: - print( - f"You do not have permission to view a repository from: '{repos_and_owners_string}'; Check your API Token." - ) - sys.exit(1) - except github3.exceptions.NotFoundError: - print( - f"The repository could not be found; Check the repository owner and names: '{repos_and_owners_string}" - ) - sys.exit(1) - except github3.exceptions.ConnectionError: - print( - "There was a connection error; Check your internet connection or API Token." - ) - sys.exit(1) - except github3.exceptions.AuthenticationFailed: - print("Authentication failed; Check your API Token.") - sys.exit(1) - except github3.exceptions.UnprocessableEntity: - print("The search query is invalid; Check the search query.") - sys.exit(1) - - return issues - - def get_per_issue_metrics( issues: Union[List[dict], List[github3.search.IssueSearchResult]], # type: ignore env_vars: EnvVars, @@ -264,37 +165,6 @@ def get_per_issue_metrics( return issues_with_metrics, num_issues_open, num_issues_closed -def get_owners_and_repositories( - search_query: str, -) -> List[dict]: - """Get the owners and repositories from the search query. - - Args: - search_query (str): The search query used to search for issues. - - Returns: - List[dict]: A list of dictionaries of owners and repositories. - - """ - search_query_split = search_query.split(" ") - results_list = [] - for item in search_query_split: - result = {} - if "repo:" in item and "/" in item: - result["owner"] = item.split(":")[1].split("/")[0] - result["repository"] = item.split(":")[1].split("/")[1] - if "org:" in item or "owner:" in item or "user:" in item: - result["owner"] = item.split(":")[1] - if "user:" in item: - result["owner"] = item.split(":")[1] - if "owner:" in item: - result["owner"] = item.split(":")[1] - if result: - results_list.append(result) - - return results_list - - def main(): # pragma: no cover """Run the issue-metrics script. diff --git a/search.py b/search.py new file mode 100644 index 00000000..5a5c1fa5 --- /dev/null +++ b/search.py @@ -0,0 +1,138 @@ +""" A module to search for issues in a GitHub repository.""" + +import sys +from time import sleep +from typing import List + +import github3 +import github3.structs + + +def search_issues( + search_query: str, + github_connection: github3.GitHub, + owners_and_repositories: List[dict], + rate_limit_bypass: bool = False, +) -> List[github3.search.IssueSearchResult]: # type: ignore + """ + Searches for issues/prs/discussions in a GitHub repository that match + the given search query and handles errors related to GitHub API responses. + + Args: + search_query (str): The search query to use for finding issues/prs/discussions. + github_connection (github3.GitHub): A connection to the GitHub API. + owners_and_repositories (List[dict]): A list of dictionaries containing + the owner and repository names. + rate_limit_bypass (bool, optional): A flag to bypass the rate limit to be used + when working with GitHub server that has rate limiting turned off. Defaults to False. + + Returns: + List[github3.search.IssueSearchResult]: A list of issues that match the search query. + """ + + # Rate Limit Handling: API only allows 30 requests per minute + def wait_for_api_refresh( + iterator: github3.structs.SearchIterator, rate_limit_bypass: bool = False + ): + # If the rate limit bypass is enabled, don't wait for the API to refresh + if rate_limit_bypass: + return + + max_retries = 5 + retry_count = 0 + sleep_time = 70 + + while iterator.ratelimit_remaining < 5: + if retry_count >= max_retries: + raise RuntimeError("Exceeded maximum retries for API rate limit") + + print( + f"GitHub API Rate Limit Low, waiting {sleep_time} seconds to refresh." + ) + sleep(sleep_time) + + # Exponentially increase the sleep time for the next retry + sleep_time *= 2 + retry_count += 1 + + issues_per_page = 100 + + print("Searching for issues...") + issues_iterator = github_connection.search_issues( + search_query, per_page=issues_per_page + ) + wait_for_api_refresh(issues_iterator, rate_limit_bypass) + + issues = [] + repos_and_owners_string = "" + for item in owners_and_repositories: + repos_and_owners_string += ( + f"{item.get('owner', '')}/{item.get('repository', '')} " + ) + + # Print the issue titles and add them to the list of issues + try: + for idx, issue in enumerate(issues_iterator, 1): + print(issue.title) # type: ignore + issues.append(issue) + + # requests are sent once per page of issues + if idx % issues_per_page == 0: + wait_for_api_refresh(issues_iterator, rate_limit_bypass) + + except github3.exceptions.ForbiddenError: + print( + f"You do not have permission to view a repository \ +from: '{repos_and_owners_string}'; Check your API Token." + ) + sys.exit(1) + except github3.exceptions.NotFoundError: + print( + f"The repository could not be found; \ +Check the repository owner and names: '{repos_and_owners_string}" + ) + sys.exit(1) + except github3.exceptions.ConnectionError: + print( + "There was a connection error; Check your internet connection or API Token." + ) + sys.exit(1) + except github3.exceptions.AuthenticationFailed: + print("Authentication failed; Check your API Token.") + sys.exit(1) + except github3.exceptions.UnprocessableEntity: + print("The search query is invalid; Check the search query.") + sys.exit(1) + + return issues + + +def get_owners_and_repositories( + search_query: str, +) -> List[dict]: + """Get the owners and repositories from the search query. + + Args: + search_query (str): The search query used to search for issues. + + Returns: + List[dict]: A list of dictionaries of owners and repositories. + + """ + search_query_split = search_query.split(" ") + results_list = [] + for item in search_query_split: + result = {} + if "repo:" in item and "/" in item: + result["owner"] = item.split(":")[1].split("/")[0] + result["repository"] = item.split(":")[1].split("/")[1] + if "org:" in item or "owner:" in item or "user:" in item: + result["owner"] = item.split(":")[1] + if "user:" in item: + result["owner"] = item.split(":")[1] + if "owner:" in item: + result["owner"] = item.split(":")[1] + if result: + results_list.append(result) + + return results_list diff --git a/test_issue_metrics.py b/test_issue_metrics.py index ab025af9..9892cca7 100644 --- a/test_issue_metrics.py +++ b/test_issue_metrics.py @@ -19,153 +19,12 @@ from issue_metrics import ( IssueWithMetrics, get_env_vars, - get_owners_and_repositories, get_per_issue_metrics, measure_time_to_close, measure_time_to_first_response, - search_issues, ) -class TestSearchIssues(unittest.TestCase): - """Unit tests for the search_issues function. - - This class contains unit tests for the search_issues function in the - issue_metrics module. The tests use the unittest module and the unittest.mock - module to mock the GitHub API and test the function in isolation. - - Methods: - test_search_issues_with_owner_and_repository: Test that search_issues with owner/repo returns the correct issues. - test_search_issues_with_just_owner_or_org: Test that search_issues with just an owner/org returns the correct issues. - - """ - - def test_search_issues_with_owner_and_repository(self): - """Test that search_issues with owner/repo returns the correct issues.""" - - # Set up the mock GitHub connection object - mock_issues = [ - MagicMock(title="Issue 1"), - MagicMock(title="Issue 2"), - ] - - # simulating github3.structs.SearchIterator return value - mock_search_result = MagicMock() - mock_search_result.__iter__.return_value = iter(mock_issues) - mock_search_result.ratelimit_remaining = 30 - - mock_connection = MagicMock() - mock_connection.search_issues.return_value = mock_search_result - - # Call search_issues and check that it returns the correct issues - repo_with_owner = {"owner": "owner1", "repository": "repo1"} - owners_and_repositories = [repo_with_owner] - issues = search_issues("is:open", mock_connection, owners_and_repositories) - self.assertEqual(issues, mock_issues) - - def test_search_issues_with_just_owner_or_org(self): - """Test that search_issues with just an owner/org returns the correct issues.""" - - # Set up the mock GitHub connection object - mock_issues = [ - MagicMock(title="Issue 1"), - MagicMock(title="Issue 2"), - MagicMock(title="Issue 3"), - ] - - # simulating github3.structs.SearchIterator return value - mock_search_result = MagicMock() - mock_search_result.__iter__.return_value = iter(mock_issues) - mock_search_result.ratelimit_remaining = 30 - - mock_connection = MagicMock() - mock_connection.search_issues.return_value = mock_search_result - - # Call search_issues and check that it returns the correct issues - org = {"owner": "org1"} - owners = [org] - issues = search_issues("is:open", mock_connection, owners) - self.assertEqual(issues, mock_issues) - - def test_search_issues_with_just_owner_or_org_with_bypass(self): - """Test that search_issues with just an owner/org returns the correct issues.""" - - # Set up the mock GitHub connection object - mock_issues = [ - MagicMock(title="Issue 1"), - MagicMock(title="Issue 2"), - MagicMock(title="Issue 3"), - ] - - # simulating github3.structs.SearchIterator return value - mock_search_result = MagicMock() - mock_search_result.__iter__.return_value = iter(mock_issues) - mock_search_result.ratelimit_remaining = 30 - - mock_connection = MagicMock() - mock_connection.search_issues.return_value = mock_search_result - - # Call search_issues and check that it returns the correct issues - org = {"owner": "org1"} - owners = [org] - issues = search_issues( - "is:open", mock_connection, owners, rate_limit_bypass=True - ) - self.assertEqual(issues, mock_issues) - - -class TestGetOwnerAndRepository(unittest.TestCase): - """Unit tests for the get_owners_and_repositories function. - - This class contains unit tests for the get_owners_and_repositories function in the - issue_metrics module. The tests use the unittest module and the unittest.mock - module to mock the GitHub API and test the function in isolation. - - Methods: - test_get_owners_with_owner_and_repo_in_query: Test get both owner and repo. - test_get_owners_and_repositories_with_repo_in_query: Test get just owner. - test_get_owners_and_repositories_without_either_in_query: Test get neither. - test_get_owners_and_repositories_with_multiple_entries: Test get multiple entries. - """ - - def test_get_owners_with_owner_and_repo_in_query(self): - """Test get both owner and repo.""" - result = get_owners_and_repositories("repo:owner1/repo1") - self.assertEqual(result[0].get("owner"), "owner1") - self.assertEqual(result[0].get("repository"), "repo1") - - def test_get_owner_and_repositories_without_repo_in_query(self): - """Test get just owner.""" - result = get_owners_and_repositories("org:owner1") - self.assertEqual(result[0].get("owner"), "owner1") - self.assertIsNone(result[0].get("repository")) - - def test_get_owners_and_repositories_without_either_in_query(self): - """Test get neither.""" - result = get_owners_and_repositories("is:blah") - self.assertEqual(result, []) - - def test_get_owners_and_repositories_with_multiple_entries(self): - """Test get multiple entries.""" - result = get_owners_and_repositories("repo:owner1/repo1 org:owner2") - self.assertEqual(result[0].get("owner"), "owner1") - self.assertEqual(result[0].get("repository"), "repo1") - self.assertEqual(result[1].get("owner"), "owner2") - self.assertIsNone(result[1].get("repository")) - - def test_get_owners_and_repositories_with_org(self): - """Test get org as owner.""" - result = get_owners_and_repositories("org:owner1") - self.assertEqual(result[0].get("owner"), "owner1") - self.assertIsNone(result[0].get("repository")) - - def test_get_owners_and_repositories_with_user(self): - """Test get user as owner.""" - result = get_owners_and_repositories("user:owner1") - self.assertEqual(result[0].get("owner"), "owner1") - self.assertIsNone(result[0].get("repository")) - - class TestGetEnvVars(unittest.TestCase): """Test suite for the get_env_vars function.""" @@ -426,7 +285,8 @@ def test_get_per_issue_metrics_without_hide_envs(self): ) def test_get_per_issue_metrics_with_ignore_users(self): """ - Test that the function correctly filters out issues with authors in the IGNORE_USERS variable + Test that the function correctly filters out issues + with authors in the IGNORE_USERS variable """ # Create mock data diff --git a/test_search.py b/test_search.py new file mode 100644 index 00000000..bd1d36f9 --- /dev/null +++ b/test_search.py @@ -0,0 +1,152 @@ +"""Unit tests for the search module.""" + +import unittest +from unittest.mock import MagicMock + +from search import get_owners_and_repositories, search_issues + + +class TestSearchIssues(unittest.TestCase): + """Unit tests for the search_issues function. + + This class contains unit tests for the search_issues function in the + issue_metrics module. The tests use the unittest module and the unittest.mock + module to mock the GitHub API and test the function in isolation. + + Methods: + test_search_issues_with_owner_and_repository: + Test that search_issues with owner/repo returns the correct issues. + test_search_issues_with_just_owner_or_org: + Test that search_issues with just an owner/org returns the correct issues. + test_search_issues_with_just_owner_or_org_with_bypass: + Test that search_issues with just an owner/org returns the correct issues + with rate limit bypass enabled. + + """ + + def test_search_issues_with_owner_and_repository(self): + """Test that search_issues with owner/repo returns the correct issues.""" + + # Set up the mock GitHub connection object + mock_issues = [ + MagicMock(title="Issue 1"), + MagicMock(title="Issue 2"), + ] + + # simulating github3.structs.SearchIterator return value + mock_search_result = MagicMock() + mock_search_result.__iter__.return_value = iter(mock_issues) + mock_search_result.ratelimit_remaining = 30 + + mock_connection = MagicMock() + mock_connection.search_issues.return_value = mock_search_result + + # Call search_issues and check that it returns the correct issues + repo_with_owner = {"owner": "owner1", "repository": "repo1"} + owners_and_repositories = [repo_with_owner] + issues = search_issues("is:open", mock_connection, owners_and_repositories) + self.assertEqual(issues, mock_issues) + + def test_search_issues_with_just_owner_or_org(self): + """Test that search_issues with just an owner/org returns the correct issues.""" + + # Set up the mock GitHub connection object + mock_issues = [ + MagicMock(title="Issue 1"), + MagicMock(title="Issue 2"), + MagicMock(title="Issue 3"), + ] + + # simulating github3.structs.SearchIterator return value + mock_search_result = MagicMock() + mock_search_result.__iter__.return_value = iter(mock_issues) + mock_search_result.ratelimit_remaining = 30 + + mock_connection = MagicMock() + mock_connection.search_issues.return_value = mock_search_result + + # Call search_issues and check that it returns the correct issues + org = {"owner": "org1"} + owners = [org] + issues = search_issues("is:open", mock_connection, owners) + self.assertEqual(issues, mock_issues) + + def test_search_issues_with_just_owner_or_org_with_bypass(self): + """Test that search_issues with just an owner/org returns the correct issues.""" + + # Set up the mock GitHub connection object + mock_issues = [ + MagicMock(title="Issue 1"), + MagicMock(title="Issue 2"), + MagicMock(title="Issue 3"), + ] + + # simulating github3.structs.SearchIterator return value + mock_search_result = MagicMock() + mock_search_result.__iter__.return_value = iter(mock_issues) + mock_search_result.ratelimit_remaining = 30 + + mock_connection = MagicMock() + mock_connection.search_issues.return_value = mock_search_result + + # Call search_issues and check that it returns the correct issues + org = {"owner": "org1"} + owners = [org] + issues = search_issues( + "is:open", mock_connection, owners, rate_limit_bypass=True + ) + self.assertEqual(issues, mock_issues) + + +class TestGetOwnerAndRepository(unittest.TestCase): + """Unit tests for the get_owners_and_repositories function. + + This class contains unit tests for the get_owners_and_repositories function in the + issue_metrics module. The tests use the unittest module and the unittest.mock + module to mock the GitHub API and test the function in isolation. + + Methods: + test_get_owners_with_owner_and_repo_in_query: Test get both owner and repo. + test_get_owner_and_repositories_without_repo_in_query: Test get just owner. + test_get_owners_and_repositories_without_either_in_query: Test get neither. + test_get_owners_and_repositories_with_multiple_entries: Test get multiple entries. + test_get_owners_and_repositories_with_org: Test get org as owner. + test_get_owners_and_repositories_with_user: Test get user as owner. + """ + + def test_get_owners_with_owner_and_repo_in_query(self): + """Test get both owner and repo.""" + result = get_owners_and_repositories("repo:owner1/repo1") + self.assertEqual(result[0].get("owner"), "owner1") + self.assertEqual(result[0].get("repository"), "repo1") + + def test_get_owner_and_repositories_without_repo_in_query(self): + """Test get just owner.""" + result = get_owners_and_repositories("org:owner1") + self.assertEqual(result[0].get("owner"), "owner1") + self.assertIsNone(result[0].get("repository")) + + def test_get_owners_and_repositories_without_either_in_query(self): + """Test get neither.""" + result = get_owners_and_repositories("is:blah") + self.assertEqual(result, []) + + def test_get_owners_and_repositories_with_multiple_entries(self): + """Test get multiple entries.""" + result = get_owners_and_repositories("repo:owner1/repo1 org:owner2") + self.assertEqual(result[0].get("owner"), "owner1") + self.assertEqual(result[0].get("repository"), "repo1") + self.assertEqual(result[1].get("owner"), "owner2") + self.assertIsNone(result[1].get("repository")) + + def test_get_owners_and_repositories_with_org(self): + """Test get org as owner.""" + result = get_owners_and_repositories("org:owner1") + self.assertEqual(result[0].get("owner"), "owner1") + self.assertIsNone(result[0].get("repository")) + + def test_get_owners_and_repositories_with_user(self): + """Test get user as owner.""" + result = get_owners_and_repositories("user:owner1") + self.assertEqual(result[0].get("owner"), "owner1") + self.assertIsNone(result[0].get("repository")) From f282e90ad6e70e4361689f2d91302a0369b660f7 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Mon, 21 Oct 2024 17:16:22 +0000 Subject: [PATCH 180/358] fix: Correct positional args for write_to_markdown() Signed-off-by: Zack Koppert --- issue_metrics.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index 7a41f574..e11d15ce 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -186,6 +186,7 @@ def main(): # pragma: no cover token = env_vars.gh_token ignore_users = env_vars.ignore_users hide_items_closed_count = env_vars.hide_items_closed_count + hide_label_metrics = env_vars.hide_label_metrics non_mentioning_links = env_vars.non_mentioning_links report_title = env_vars.report_title output_file = env_vars.output_file @@ -322,20 +323,21 @@ def main(): # pragma: no cover ) write_to_markdown( - issues_with_metrics, - stats_time_to_first_response, - stats_time_to_close, - stats_time_to_answer, - stats_time_in_labels, - num_issues_open, - num_issues_closed, - num_mentor_count, - labels, - search_query, - hide_items_closed_count, - non_mentioning_links, - report_title, - output_file, + issues_with_metrics=issues_with_metrics, + average_time_to_first_response=stats_time_to_first_response, + average_time_to_close=stats_time_to_close, + average_time_to_answer=stats_time_to_answer, + average_time_in_labels=stats_time_in_labels, + num_issues_opened=num_issues_open, + num_issues_closed=num_issues_closed, + num_mentor_count=num_mentor_count, + labels=labels, + search_query=search_query, + hide_label_metrics=hide_label_metrics, + hide_items_closed_count=hide_items_closed_count, + non_mentioning_links=non_mentioning_links, + report_title=report_title, + output_file=output_file, ) max_char_count = 65535 From 8c300b62ce7de0b30241d81392fe59f86d094ea3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 17:25:20 +0000 Subject: [PATCH 181/358] chore(deps): bump mypy from 1.12.0 to 1.12.1 in the dependencies group Bumps the dependencies group with 1 update: [mypy](https://github.com/python/mypy). Updates `mypy` from 1.12.0 to 1.12.1 - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.12.0...v1.12.1) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 1d5ee6d0..ab57721b 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,6 +1,6 @@ black==24.10.0 flake8==7.1.1 -mypy==1.12.0 +mypy==1.12.1 mypy-extensions==1.0.0 pylint==3.3.1 pytest==8.3.3 From 511482b204571a4c1d029109bbd22f7261520e27 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 17:25:23 +0000 Subject: [PATCH 182/358] chore(deps): bump types-requests from 2.32.0.20240914 to 2.32.0.20241016 Bumps [types-requests](https://github.com/python/typeshed) from 2.32.0.20240914 to 2.32.0.20241016. - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 1d5ee6d0..d315f1ed 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -6,4 +6,4 @@ pylint==3.3.1 pytest==8.3.3 pytest-cov==5.0.0 types-pytz==2024.2.0.20241003 -types-requests==2.32.0.20240914 +types-requests==2.32.0.20241016 From f04af07b5f7f58ac87c939f293bfef12b2266b25 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 17:56:09 +0000 Subject: [PATCH 183/358] chore(deps): bump python from `2ec5a4a` to `751d8be` Bumps python from `2ec5a4a` to `751d8be`. --- updated-dependencies: - dependency-name: python dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 434656f3..b7a32970 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.13-slim@sha256:2ec5a4a5c3e919570f57675471f081d6299668d909feabd8d4803c6c61af666c +FROM python:3.13-slim@sha256:751d8bece269ba9e672b3f2226050e7e6fb3f3da3408b5dcb5d415a054fcb061 LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From 45b3d723ca0c28b499c5bcb3695f49ab80d26e5e Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 22 Oct 2024 14:39:58 -0700 Subject: [PATCH 184/358] docs: rm stale inline docs Signed-off-by: Zack Koppert --- issue_metrics.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index e11d15ce..a33042be 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -6,8 +6,6 @@ their metrics to a markdown file. Functions: - get_env_vars() -> EnvVars: Get the environment variables for use - in the script. get_per_issue_metrics(issues: Union[List[dict], List[github3.issues.Issue]], discussions: bool = False), labels: Union[List[str], None] = None, ignore_users: List[str] = [] -> tuple[List, int, int]: From d8dd50ada1ffc4fe8288a087e94ce5c79fee0abb Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 22 Oct 2024 17:03:25 -0700 Subject: [PATCH 185/358] feat: add draft pr tracking environment variable Signed-off-by: Zack Koppert --- config.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/config.py b/config.py index 4d544e4b..5de753f5 100644 --- a/config.py +++ b/config.py @@ -1,6 +1,7 @@ """A module for managing environment variables used in GitHub metrics calculation. -This module defines a class for encapsulating environment variables and a function to retrieve these variables. +This module defines a class for encapsulating environment variables +and a function to retrieve these variables. Classes: EnvVars: Represents the collection of environment variables used in the script. @@ -23,27 +24,36 @@ class EnvVars: Attributes: gh_app_id (int | None): The GitHub App ID to use for authentication - gh_app_installation_id (int | None): The GitHub App Installation ID to use for authentication - gh_app_private_key_bytes (bytes): The GitHub App Private Key as bytes to use for authentication + gh_app_installation_id (int | None): The GitHub App Installation ID to use for + authentication + gh_app_private_key_bytes (bytes): The GitHub App Private Key as bytes to use for + authentication gh_token (str | None): GitHub personal access token (PAT) for API authentication ghe (str): The GitHub Enterprise URL to use for authentication hide_author (bool): If true, the author's information is hidden in the output - hide_items_closed_count (bool): If true, the number of items closed metric is hidden in the output + hide_items_closed_count (bool): If true, the number of items closed metric is hidden + in the output hide_label_metrics (bool): If true, the label metrics are hidden in the output hide_time_to_answer (bool): If true, the time to answer discussions is hidden in the output hide_time_to_close (bool): If true, the time to close metric is hidden in the output - hide_time_to_first_response (bool): If true, the time to first response metric is hidden in the output + hide_time_to_first_response (bool): If true, the time to first response metric is hidden + in the output ignore_users (List[str]): List of usernames to ignore when calculating metrics labels_to_measure (List[str]): List of labels to measure how much time the lable is applied enable_mentor_count (bool): If set to TRUE, compute number of mentors min_mentor_comments (str): If set, defines the minimum number of comments for mentors - max_comments_eval (str): If set, defines the maximum number of comments to look at for mentor evaluation - heavily_involved_cutoff (str): If set, defines the cutoff after which heavily involved commentors in + max_comments_eval (str): If set, defines the maximum number of comments to look + at for mentor evaluation + heavily_involved_cutoff (str): If set, defines the cutoff after which heavily + involved commentors in search_query (str): Search query used to filter issues/prs/discussions on GitHub - non_mentioning_links (bool): If set to TRUE, links do not cause a notification in the desitnation repository + non_mentioning_links (bool): If set to TRUE, links do not cause a notification + in the desitnation repository report_title (str): The title of the report output_file (str): The name of the file to write the report to rate_limit_bypass (bool): If set to TRUE, bypass the rate limit for the GitHub API + draft_pr_tracking (bool): If set to TRUE, track PR time in draft state + in addition to other metrics """ def __init__( @@ -70,6 +80,7 @@ def __init__( report_title: str, output_file: str, rate_limit_bypass: bool = False, + draft_pr_tracking: bool = False, ): self.gh_app_id = gh_app_id self.gh_app_installation_id = gh_app_installation_id @@ -93,6 +104,7 @@ def __init__( self.report_title = report_title self.output_file = output_file self.rate_limit_bypass = rate_limit_bypass + self.draft_pr_tracking = draft_pr_tracking def __repr__(self): return ( @@ -119,6 +131,7 @@ def __repr__(self): f"{self.report_title}" f"{self.output_file}" f"{self.rate_limit_bypass}" + f"{self.draft_pr_tracking}" ) @@ -203,6 +216,7 @@ def get_env_vars(test: bool = False) -> EnvVars: report_title = os.getenv("REPORT_TITLE", "Issue Metrics") output_file = os.getenv("OUTPUT_FILE", "") rate_limit_bypass = get_bool_env_var("RATE_LIMIT_BYPASS", False) + draft_pr_tracking = get_bool_env_var("DRAFT_PR_TRACKING", False) # Hidden columns hide_author = get_bool_env_var("HIDE_AUTHOR", False) @@ -240,4 +254,5 @@ def get_env_vars(test: bool = False) -> EnvVars: report_title, output_file, rate_limit_bypass, + draft_pr_tracking, ) From da8008c78d7f3ec555829dd4ece9a3a8abd41ed9 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 22 Oct 2024 17:04:20 -0700 Subject: [PATCH 186/358] chore: use keyword args instead of positional Signed-off-by: Zack Koppert --- test_config.py | 174 +++++++++++++++++++++++++------------------------ 1 file changed, 89 insertions(+), 85 deletions(-) diff --git a/test_config.py b/test_config.py index ea7ddb28..e6d85f63 100644 --- a/test_config.py +++ b/test_config.py @@ -116,28 +116,28 @@ def setUp(self): def test_get_env_vars_with_github_app(self): """Test that all environment variables are set correctly using GitHub App""" expected_result = EnvVars( - 12345, - 678910, - b"hello", - "", - "", - False, - False, - False, - False, - False, - False, - [], - [], - False, - "10", - "20", - "3", - SEARCH_QUERY, - False, - "", - "", - False, + gh_app_id=12345, + gh_app_installation_id=678910, + gh_app_private_key_bytes=b"hello", + gh_token="", + ghe="", + hide_author=False, + hide_items_closed_count=False, + hide_label_metrics=False, + hide_time_to_answer=False, + hide_time_to_close=False, + hide_time_to_first_response=False, + ignore_user=[], + labels_to_measure=[], + enable_mentor_count=False, + min_mentor_comments="10", + max_comments_eval="20", + heavily_involved_cutoff="3", + search_query=SEARCH_QUERY, + non_mentioning_links=False, + report_title="", + output_file="", + draft_pr_tracking=False, ) result = get_env_vars(True) self.assertEqual(str(result), str(expected_result)) @@ -168,27 +168,27 @@ def test_get_env_vars_with_github_app(self): def test_get_env_vars_with_token(self): """Test that all environment variables are set correctly using a list of repositories""" expected_result = EnvVars( - None, - None, - b"", - TOKEN, - "", - False, - False, - False, - False, - False, - False, - [], - [], - False, - "10", - "20", - "3", - SEARCH_QUERY, - False, - "", - "", + gh_app_id=None, + gh_app_installation_id=None, + gh_app_private_key_bytes=b"", + gh_token=TOKEN, + ghe="", + hide_author=False, + hide_items_closed_count=False, + hide_label_metrics=False, + hide_time_to_answer=False, + hide_time_to_close=False, + hide_time_to_first_response=False, + ignore_user=[], + labels_to_measure=[], + enable_mentor_count=False, + min_mentor_comments="10", + max_comments_eval="20", + heavily_involved_cutoff="3", + search_query=SEARCH_QUERY, + non_mentioning_links=False, + report_title="", + output_file="", ) result = get_env_vars(True) self.assertEqual(str(result), str(expected_result)) @@ -248,32 +248,36 @@ def test_get_env_vars_missing_query(self): "OUTPUT_FILE": "issue_metrics.md", "REPORT_TITLE": "Issue Metrics", "SEARCH_QUERY": SEARCH_QUERY, + "RATE_LIMIT_BYPASS": "true", + "DRAFT_PR_TRACKING": "True", }, ) def test_get_env_vars_optional_values(self): """Test that optional values are set to their default values if not provided""" expected_result = EnvVars( - None, - None, - b"", - TOKEN, - "", - True, - True, - True, - True, - True, - True, - [], - ["waiting-for-review", "waiting-for-manager"], - False, - 10, - 20, - 3, - SEARCH_QUERY, - True, - "Issue Metrics", - "issue_metrics.md", + gh_app_id=None, + gh_app_installation_id=None, + gh_app_private_key_bytes=b"", + gh_token=TOKEN, + ghe="", + hide_author=True, + hide_items_closed_count=True, + hide_label_metrics=True, + hide_time_to_answer=True, + hide_time_to_close=True, + hide_time_to_first_response=True, + ignore_user=[], + labels_to_measure=["waiting-for-review", "waiting-for-manager"], + enable_mentor_count=False, + min_mentor_comments=10, + max_comments_eval=20, + heavily_involved_cutoff=3, + search_query=SEARCH_QUERY, + non_mentioning_links=True, + report_title="Issue Metrics", + output_file="issue_metrics.md", + rate_limit_bypass=True, + draft_pr_tracking=True, ) result = get_env_vars(True) self.assertEqual(str(result), str(expected_result)) @@ -292,27 +296,27 @@ def test_get_env_vars_optional_values(self): def test_get_env_vars_optionals_are_defaulted(self): """Test that optional values are set to their default values if not provided""" expected_result = EnvVars( - None, - None, - b"", - "TOKEN", - "", - False, - False, - False, - False, - False, - False, - [], - [], - False, - "10", - "20", - "3", - SEARCH_QUERY, - False, - "Issue Metrics", - "", + gh_app_id=None, + gh_app_installation_id=None, + gh_app_private_key_bytes=b"", + gh_token="TOKEN", + ghe="", + hide_author=False, + hide_items_closed_count=False, + hide_label_metrics=False, + hide_time_to_answer=False, + hide_time_to_close=False, + hide_time_to_first_response=False, + ignore_user=[], + labels_to_measure=[], + enable_mentor_count=False, + min_mentor_comments="10", + max_comments_eval="20", + heavily_involved_cutoff="3", + search_query=SEARCH_QUERY, + non_mentioning_links=False, + report_title="Issue Metrics", + output_file="", ) result = get_env_vars(True) self.assertEqual(str(result), str(expected_result)) From f968e7ba42bfb2d6d440b3d410aba0f475c39219 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 22 Oct 2024 17:06:00 -0700 Subject: [PATCH 187/358] test: ensure defaults for pr tracking are respected Signed-off-by: Zack Koppert --- test_config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test_config.py b/test_config.py index e6d85f63..dab4e7d2 100644 --- a/test_config.py +++ b/test_config.py @@ -317,6 +317,8 @@ def test_get_env_vars_optionals_are_defaulted(self): non_mentioning_links=False, report_title="Issue Metrics", output_file="", + rate_limit_bypass=False, + draft_pr_tracking=False, ) result = get_env_vars(True) self.assertEqual(str(result), str(expected_result)) From 2f684840d106121534a060be42188f213cf02f0b Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Wed, 23 Oct 2024 08:11:09 -0700 Subject: [PATCH 188/358] feat: Add time in draft to IssueWithMetrics class attributes and update failing tests that were using positional args Signed-off-by: Zack Koppert --- classes.py | 3 +++ test_labels.py | 8 +++++- test_markdown_writer.py | 56 ++++++++++++++++++++--------------------- 3 files changed, 38 insertions(+), 29 deletions(-) diff --git a/classes.py b/classes.py index e1e6684d..7428d31e 100644 --- a/classes.py +++ b/classes.py @@ -18,6 +18,7 @@ class IssueWithMetrics: time_to_close (timedelta, optional): The time it took to close the issue. time_to_answer (timedelta, optional): The time it took to answer the discussions in the issue. + time_in_draft (timedelta, optional): The time the PR was in draft state. label_metrics (dict, optional): A dictionary containing the label metrics mentor_activity (dict, optional): A dictionary containing active mentors @@ -33,6 +34,7 @@ def __init__( time_to_first_response=None, time_to_close=None, time_to_answer=None, + time_in_draft=None, labels_metrics=None, mentor_activity=None, ): @@ -42,5 +44,6 @@ def __init__( self.time_to_first_response = time_to_first_response self.time_to_close = time_to_close self.time_to_answer = time_to_answer + self.time_in_draft = time_in_draft self.label_metrics = labels_metrics self.mentor_activity = mentor_activity diff --git a/test_labels.py b/test_labels.py index e7e8b5f7..2fb82121 100644 --- a/test_labels.py +++ b/test_labels.py @@ -101,7 +101,13 @@ def setUp(self): self.issues_with_metrics = MagicMock() self.issues_with_metrics = [ IssueWithMetrics( - "issue1", "url1", "alice", None, None, None, {"bug": timedelta(days=2)} + title="issue1", + html_url="url1", + author="alice", + time_to_first_response=None, + time_to_close=None, + time_to_answer=None, + labels_metrics={"bug": timedelta(days=2)}, ), ] diff --git a/test_markdown_writer.py b/test_markdown_writer.py index 3678d0ba..8d69d355 100644 --- a/test_markdown_writer.py +++ b/test_markdown_writer.py @@ -37,22 +37,22 @@ def test_write_to_markdown(self): # Create mock data issues_with_metrics = [ IssueWithMetrics( - "Issue 1", - "https://github.com/user/repo/issues/1", - "alice", - timedelta(days=1), - timedelta(days=2), - timedelta(days=3), - {"bug": timedelta(days=1)}, + title="Issue 1", + html_url="https://github.com/user/repo/issues/1", + author="alice", + time_to_first_response=timedelta(days=1), + time_to_close=timedelta(days=2), + time_to_answer=timedelta(days=3), + labels_metrics={"bug": timedelta(days=1)}, ), IssueWithMetrics( - "Issue 2\r", - "https://github.com/user/repo/issues/2", - "bob", - timedelta(days=3), - timedelta(days=4), - timedelta(days=5), - {"bug": timedelta(days=2)}, + title="Issue 2\r", + html_url="https://github.com/user/repo/issues/2", + author="bob", + time_to_first_response=timedelta(days=3), + time_to_close=timedelta(days=4), + time_to_answer=timedelta(days=5), + labels_metrics={"bug": timedelta(days=2)}, ), ] time_to_first_response = { @@ -139,22 +139,22 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): # Create mock data issues_with_metrics = [ IssueWithMetrics( - "Issue 1", - "https://github.com/user/repo/issues/1", - "alice", - timedelta(days=1), - timedelta(days=2), - timedelta(days=3), - {"bug": timedelta(days=1)}, + title="Issue 1", + html_url="https://github.com/user/repo/issues/1", + author="alice", + time_to_first_response=timedelta(days=1), + time_to_close=timedelta(days=2), + time_to_answer=timedelta(days=3), + labels_metrics={"bug": timedelta(days=1)}, ), IssueWithMetrics( - "feat| Issue 2", # title contains a vertical bar - "https://github.com/user/repo/issues/2", - "bob", - timedelta(days=3), - timedelta(days=4), - timedelta(days=5), - {"bug": timedelta(days=2)}, + title="feat| Issue 2", # title contains a vertical bar + html_url="https://github.com/user/repo/issues/2", + author="bob", + time_to_first_response=timedelta(days=3), + time_to_close=timedelta(days=4), + time_to_answer=timedelta(days=5), + labels_metrics={"bug": timedelta(days=2)}, ), ] average_time_to_first_response = { From 68a01b4929aee0e0c9bc0fef53b0083e5c894446 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Wed, 23 Oct 2024 11:38:08 -0700 Subject: [PATCH 189/358] feat: add measure_time_in_draft() and tests Signed-off-by: Zack Koppert --- issue_metrics.py | 22 +++++++------- test_time_in_draft.py | 60 +++++++++++++++++++++++++++++++++++++ time_in_draft.py | 43 ++++++++++++++++++++++++++ time_to_ready_for_review.py | 3 +- 4 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 test_time_in_draft.py create mode 100644 time_in_draft.py diff --git a/issue_metrics.py b/issue_metrics.py index a33042be..a534f9cb 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -30,6 +30,7 @@ from markdown_writer import write_to_markdown from most_active_mentors import count_comments_per_user, get_mentor_count from search import get_owners_and_repositories, search_issues +from time_in_draft import measure_time_in_draft from time_to_answer import get_stats_time_to_answer, measure_time_to_answer from time_to_close import get_stats_time_to_close, measure_time_to_close from time_to_first_response import ( @@ -112,13 +113,9 @@ def get_per_issue_metrics( continue issue_with_metrics = IssueWithMetrics( - issue.title, # type: ignore - issue.html_url, # type: ignore - issue.user["login"], # type: ignore - None, - None, - None, - None, + title=issue.title, # type: ignore + html_url=issue.html_url, # type: ignore + author=issue.user["login"], # type: ignore ) # Check if issue is actually a pull request @@ -126,6 +123,11 @@ def get_per_issue_metrics( if issue.issue.pull_request_urls: # type: ignore pull_request = issue.issue.pull_request() # type: ignore ready_for_review_at = get_time_to_ready_for_review(issue, pull_request) + if env_vars.draft_pr_tracking: + issue_with_metrics.time_in_draft = measure_time_in_draft( + issue=issue, + ready_for_review_at=ready_for_review_at, + ) if env_vars.hide_time_to_first_response is False: issue_with_metrics.time_to_first_response = ( @@ -345,9 +347,9 @@ def main(): # pragma: no cover shutil.move("issue_metrics_0.md", "issue_metrics.md") print( "Issue metrics markdown file is too large for GitHub issue body and has been \ - split into multiple files. ie. issue_metrics.md, issue_metrics_1.md, etc. \ - The full file is saved as issue_metrics_full.md\n\ - See https://github.com/github/issue-metrics/blob/main/docs/dealing-with-large-issue-metrics.md" +split into multiple files. ie. issue_metrics.md, issue_metrics_1.md, etc. \ +The full file is saved as issue_metrics_full.md\n\ +See https://github.com/github/issue-metrics/blob/main/docs/dealing-with-large-issue-metrics.md" ) diff --git a/test_time_in_draft.py b/test_time_in_draft.py new file mode 100644 index 00000000..17ced108 --- /dev/null +++ b/test_time_in_draft.py @@ -0,0 +1,60 @@ +"""A test suite for the measure_time_in_draft function.""" + +import unittest +from datetime import datetime, timedelta +from unittest.mock import MagicMock + +import pytz +from time_in_draft import measure_time_in_draft + + +class TestMeasureTimeInDraft(unittest.TestCase): + """ + Unit tests for the measure_time_in_draft function. + """ + + def setUp(self): + """ + Setup common test data and mocks. + """ + self.issue = MagicMock() + self.issue.issue.events.return_value = [ + MagicMock( + event="created_at", created_at=datetime(2021, 1, 1, tzinfo=pytz.utc) + ), + MagicMock( + event="other_event", created_at=datetime(2021, 1, 2, tzinfo=pytz.utc) + ), + ] + + def test_time_in_draft_with_ready_for_review(self): + """ + Test measure_time_in_draft when ready_for_review_at is provided. + """ + ready_for_review_at = datetime(2021, 1, 3, tzinfo=pytz.utc) + result = measure_time_in_draft(self.issue, ready_for_review_at) + expected = timedelta(days=2) + self.assertEqual(result, expected, "The time in draft should be 2 days.") + + def test_time_in_draft_without_ready_for_review(self): + """ + Test measure_time_in_draft when ready_for_review_at is not provided. + """ + now = datetime(2021, 1, 4, tzinfo=pytz.utc) + with unittest.mock.patch("time_in_draft.datetime") as mock_datetime: + mock_datetime.now.return_value = now + result = measure_time_in_draft(self.issue, None) + expected = timedelta(days=3) + self.assertEqual(result, expected, "The time in draft should be 3 days.") + + def test_time_in_draft_with_no_events(self): + """ + Test measure_time_in_draft when there are no events. + """ + self.issue.issue.events.return_value = [] + result = measure_time_in_draft(self.issue, None) + self.assertIsNone(result, "The result should be None when there are no events.") + + +if __name__ == "__main__": + unittest.main() diff --git a/time_in_draft.py b/time_in_draft.py new file mode 100644 index 00000000..ed9f657d --- /dev/null +++ b/time_in_draft.py @@ -0,0 +1,43 @@ +""" +This module contains a function that measures the time a pull request has been in draft state. +""" + +from datetime import datetime +from typing import Union + +import github3 +import pytz + + +def measure_time_in_draft( + issue: github3.issues.Issue, + ready_for_review_at: Union[datetime, None], +) -> Union[datetime, None]: + """If a pull request has had time in the draft state, return the amount of time it was in draft. + + args: + issue (github3.issues.Issue): A GitHub issue which has been pre-qualified as a pull request. + ready_for_review_at (datetime): The time the pull request was marked as ready for review. + + returns: + Union[datetime, None]: The time the pull request was in draft state. + """ + events = issue.issue.events(number=50) + try: + pr_opened_at = None + for event in events: + if event.event == "created_at": + pr_opened_at = event.created_at + if pr_opened_at and ready_for_review_at: + return ready_for_review_at - pr_opened_at + if pr_opened_at and not ready_for_review_at: + return datetime.now(pytz.utc) - pr_opened_at + + except TypeError as e: + print( + f"An error occurred processing review events for {issue.issue.html_url}. \ +Perhaps issue contains a ghost user. {e}" + ) + return None + + return None diff --git a/time_to_ready_for_review.py b/time_to_ready_for_review.py index f95ffc31..5c5d7725 100644 --- a/time_to_ready_for_review.py +++ b/time_to_ready_for_review.py @@ -21,7 +21,8 @@ def get_time_to_ready_for_review( - issue: github3.issues.Issue, pull_request: github3.pulls.PullRequest + issue: github3.issues.Issue, + pull_request: github3.pulls.PullRequest, ) -> Union[datetime, None]: """If a pull request was formerly a draft, get the time it was marked as ready for review From edfb56eb925a0b15f7eea64ed541f89a9ea77eeb Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Wed, 23 Oct 2024 11:41:52 -0700 Subject: [PATCH 190/358] docs: update inline docs to correct type Signed-off-by: Zack Koppert --- time_in_draft.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/time_in_draft.py b/time_in_draft.py index ed9f657d..8a9f1c5e 100644 --- a/time_in_draft.py +++ b/time_in_draft.py @@ -17,7 +17,7 @@ def measure_time_in_draft( args: issue (github3.issues.Issue): A GitHub issue which has been pre-qualified as a pull request. - ready_for_review_at (datetime): The time the pull request was marked as ready for review. + ready_for_review_at (datetime | None): The time the pull request was marked as ready for review. returns: Union[datetime, None]: The time the pull request was in draft state. From fe2bc4a4071c7981025d7cbe02b6c3fb420575b0 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Wed, 23 Oct 2024 11:58:21 -0700 Subject: [PATCH 191/358] feat: add time_in_draft to json output Signed-off-by: Zack Koppert --- json_writer.py | 2 ++ test_json_writer.py | 5 +++++ time_in_draft.py | 3 ++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/json_writer.py b/json_writer.py index 0b191dc4..5655febb 100644 --- a/json_writer.py +++ b/json_writer.py @@ -55,6 +55,7 @@ def write_to_json( "time_to_first_response": "3 days, 0:00:00", "time_to_close": "6 days, 0:00:00", "time_to_answer": "None", + "time_in_draft": "1 day, 0:00:00", "label_metrics": { "bug": "1 day, 16:24:12" } @@ -152,6 +153,7 @@ def write_to_json( "time_to_first_response": str(issue.time_to_first_response), "time_to_close": str(issue.time_to_close), "time_to_answer": str(issue.time_to_answer), + "time_in_draft": str(issue.time_in_draft), "label_metrics": formatted_label_metrics, } ) diff --git a/test_json_writer.py b/test_json_writer.py index 5dae93c8..36d1faaf 100644 --- a/test_json_writer.py +++ b/test_json_writer.py @@ -24,6 +24,7 @@ def test_write_to_json(self): time_to_first_response=timedelta(days=3), time_to_close=timedelta(days=6), time_to_answer=None, + time_in_draft=timedelta(days=1), labels_metrics={ "bug": timedelta(days=1, hours=16, minutes=24, seconds=12) }, @@ -88,6 +89,7 @@ def test_write_to_json(self): "time_to_first_response": "3 days, 0:00:00", "time_to_close": "6 days, 0:00:00", "time_to_answer": "None", + "time_in_draft": "1 day, 0:00:00", "label_metrics": {"bug": "1 day, 16:24:12"}, }, { @@ -97,6 +99,7 @@ def test_write_to_json(self): "time_to_first_response": "2 days, 0:00:00", "time_to_close": "4 days, 0:00:00", "time_to_answer": "1 day, 0:00:00", + "time_in_draft": "None", "label_metrics": {}, }, ], @@ -180,6 +183,7 @@ def test_write_to_json_with_no_response(self): "time_to_first_response": "None", "time_to_close": "None", "time_to_answer": "None", + "time_in_draft": "None", "label_metrics": {}, }, { @@ -189,6 +193,7 @@ def test_write_to_json_with_no_response(self): "time_to_first_response": "None", "time_to_close": "None", "time_to_answer": "None", + "time_in_draft": "None", "label_metrics": {}, }, ], diff --git a/time_in_draft.py b/time_in_draft.py index 8a9f1c5e..cef3a9bc 100644 --- a/time_in_draft.py +++ b/time_in_draft.py @@ -17,7 +17,8 @@ def measure_time_in_draft( args: issue (github3.issues.Issue): A GitHub issue which has been pre-qualified as a pull request. - ready_for_review_at (datetime | None): The time the pull request was marked as ready for review. + ready_for_review_at (datetime | None): The time the pull request was marked as + ready for review. returns: Union[datetime, None]: The time the pull request was in draft state. From 4b6b4bca29423be92b0dbd407a33cf03b63589ff Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Wed, 23 Oct 2024 12:37:11 -0700 Subject: [PATCH 192/358] feat: add get_stats_time_in_draft in order to calculate the overall draft time stats for multiple issues Signed-off-by: Zack Koppert --- issue_metrics.py | 24 +++++++------- json_writer.py | 77 +++++++++++++++++++++++++++++-------------- test_json_writer.py | 14 ++++++++ test_time_in_draft.py | 54 +++++++++++++++++++++++++++++- time_in_draft.py | 46 ++++++++++++++++++++++++-- 5 files changed, 176 insertions(+), 39 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index a534f9cb..4d2fc961 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -30,7 +30,7 @@ from markdown_writer import write_to_markdown from most_active_mentors import count_comments_per_user, get_mentor_count from search import get_owners_and_repositories, search_issues -from time_in_draft import measure_time_in_draft +from time_in_draft import get_stats_time_in_draft, measure_time_in_draft from time_to_answer import get_stats_time_to_answer, measure_time_to_answer from time_to_close import get_stats_time_to_close, measure_time_to_close from time_to_first_response import ( @@ -299,6 +299,7 @@ def main(): # pragma: no cover stats_time_to_close = get_stats_time_to_close(issues_with_metrics) stats_time_to_answer = get_stats_time_to_answer(issues_with_metrics) + stats_time_in_draft = get_stats_time_in_draft(issues_with_metrics) num_mentor_count = 0 if enable_mentor_count: @@ -310,16 +311,17 @@ def main(): # pragma: no cover # Write the results to json and a markdown file write_to_json( - issues_with_metrics, - stats_time_to_first_response, - stats_time_to_close, - stats_time_to_answer, - stats_time_in_labels, - num_issues_open, - num_issues_closed, - num_mentor_count, - search_query, - output_file, + issues_with_metrics=issues_with_metrics, + stats_time_to_first_response=stats_time_to_first_response, + stats_time_to_close=stats_time_to_close, + stats_time_to_answer=stats_time_to_answer, + stats_time_in_draft=stats_time_in_draft, + stats_time_in_labels=stats_time_in_labels, + num_issues_opened=num_issues_open, + num_issues_closed=num_issues_closed, + num_mentor_count=num_mentor_count, + search_query=search_query, + output_file=output_file, ) write_to_markdown( diff --git a/json_writer.py b/json_writer.py index 5655febb..015dbe44 100644 --- a/json_writer.py +++ b/json_writer.py @@ -2,12 +2,15 @@ Functions: write_to_json( - issues_with_metrics: List[IssueWithMetrics], - average_time_to_first_response: timedelta, - average_time_to_close: timedelta, - average_time_to_answer: timedelta, - num_issues_opened: int, - num_issues_closed: int, + issues_with_metrics: Union[List[IssueWithMetrics], None], + stats_time_to_first_response: Union[dict[str, timedelta], None], + stats_time_to_close: Union[dict[str, timedelta], None], + stats_time_to_answer: Union[dict[str, timedelta], None], + stats_time_in_draft: Union[dict[str, timedelta], None], + stats_time_in_labels: Union[dict[str, dict[str, timedelta]], None], + num_issues_opened: Union[int, None], + num_issues_closed: Union[int, None], + num_mentor_count: Union[int, None], search_query: str, output_file: str, ) -> str: @@ -28,6 +31,7 @@ def write_to_json( stats_time_to_first_response: Union[dict[str, timedelta], None], stats_time_to_close: Union[dict[str, timedelta], None], stats_time_to_answer: Union[dict[str, timedelta], None], + stats_time_in_draft: Union[dict[str, timedelta], None], stats_time_in_labels: Union[dict[str, dict[str, timedelta]], None], num_issues_opened: Union[int, None], num_issues_closed: Union[int, None], @@ -40,38 +44,48 @@ def write_to_json( json structure is like following { - "average_time_to_first_response": "2 days, 12:00:00", - "average_time_to_close": "5 days, 0:00:00", - "average_time_to_answer": "1 day, 0:00:00", + "average_time_to_first_response": "None", + "average_time_to_close": "None", + "average_time_to_answer": "None", + "average_time_in_draft": "None", + "average_time_in_labels": {}, + "median_time_to_first_response": "None", + "median_time_to_close": "None", + "median_time_to_answer": "None", + "median_time_in_draft": "None", + "median_time_in_labels": {}, + "90_percentile_time_to_first_response": "None", + "90_percentile_time_to_close": "None", + "90_percentile_time_to_answer": "None", + "90_percentile_time_in_draft": "None", + "90_percentile_time_in_labels": {}, "num_items_opened": 2, - "num_items_closed": 1, + "num_items_closed": 0, "num_mentor_count": 5, "total_item_count": 2, "issues": [ { "title": "Issue 1", "html_url": "https://github.com/owner/repo/issues/1", - "author": "author", - "time_to_first_response": "3 days, 0:00:00", - "time_to_close": "6 days, 0:00:00", + "author": "alice", + "time_to_first_response": "None", + "time_to_close": "None", "time_to_answer": "None", - "time_in_draft": "1 day, 0:00:00", - "label_metrics": { - "bug": "1 day, 16:24:12" - } + "time_in_draft": "None", + "label_metrics": {} }, { "title": "Issue 2", "html_url": "https://github.com/owner/repo/issues/2", - "author": "author", - "time_to_first_response": "2 days, 0:00:00", - "time_to_close": "4 days, 0:00:00", - "time_to_answer": "1 day, 0:00:00", - "label_metrics": { - } - }, + "author": "bob", + "time_to_first_response": "None", + "time_to_close": "None", + "time_to_answer": "None", + "time_in_draft": "None", + "label_metrics": {} + } ], - "search_query": "is:issue is:open repo:owner/repo" + "search_query": "is:issue repo:owner/repo" } """ @@ -107,6 +121,16 @@ def write_to_json( med_time_to_answer = stats_time_to_answer["med"] p90_time_to_answer = stats_time_to_answer["90p"] + # time in draft + average_time_in_draft = None + med_time_in_draft = None + p90_time_in_draft = None + if stats_time_in_draft is not None: + average_time_in_draft = stats_time_in_draft["avg"] + med_time_in_draft = stats_time_in_draft["med"] + p90_time_in_draft = stats_time_in_draft["90p"] + + # time in labels average_time_in_labels = {} med_time_in_labels = {} p90_time_in_labels = {} @@ -123,14 +147,17 @@ def write_to_json( "average_time_to_first_response": str(average_time_to_first_response), "average_time_to_close": str(average_time_to_close), "average_time_to_answer": str(average_time_to_answer), + "average_time_in_draft": str(average_time_in_draft), "average_time_in_labels": average_time_in_labels, "median_time_to_first_response": str(med_time_to_first_response), "median_time_to_close": str(med_time_to_close), "median_time_to_answer": str(med_time_to_answer), + "median_time_in_draft": str(med_time_in_draft), "median_time_in_labels": med_time_in_labels, "90_percentile_time_to_first_response": str(p90_time_to_first_response), "90_percentile_time_to_close": str(p90_time_to_close), "90_percentile_time_to_answer": str(p90_time_to_answer), + "90_percentile_time_in_draft": str(p90_time_in_draft), "90_percentile_time_in_labels": p90_time_in_labels, "num_items_opened": num_issues_opened, "num_items_closed": num_issues_closed, diff --git a/test_json_writer.py b/test_json_writer.py index 36d1faaf..1525d6c6 100644 --- a/test_json_writer.py +++ b/test_json_writer.py @@ -55,6 +55,11 @@ def test_write_to_json(self): "med": timedelta(days=2), "90p": timedelta(days=3), } + stats_time_in_draft = { + "avg": timedelta(days=1), + "med": timedelta(days=1), + "90p": timedelta(days=1), + } stats_time_in_labels = { "avg": {"bug": timedelta(days=1, hours=16, minutes=24, seconds=12)}, "med": {"bug": timedelta(days=1, hours=16, minutes=24, seconds=12)}, @@ -68,14 +73,17 @@ def test_write_to_json(self): "average_time_to_first_response": "2 days, 12:00:00", "average_time_to_close": "5 days, 0:00:00", "average_time_to_answer": "1 day, 0:00:00", + "average_time_in_draft": "1 day, 0:00:00", "average_time_in_labels": {"bug": "1 day, 16:24:12"}, "median_time_to_first_response": "2 days, 12:00:00", "median_time_to_close": "4 days, 0:00:00", "median_time_to_answer": "2 days, 0:00:00", + "median_time_in_draft": "1 day, 0:00:00", "median_time_in_labels": {"bug": "1 day, 16:24:12"}, "90_percentile_time_to_first_response": "1 day, 12:00:00", "90_percentile_time_to_close": "3 days, 0:00:00", "90_percentile_time_to_answer": "3 days, 0:00:00", + "90_percentile_time_in_draft": "1 day, 0:00:00", "90_percentile_time_in_labels": {"bug": "1 day, 16:24:12"}, "num_items_opened": 2, "num_items_closed": 1, @@ -113,6 +121,7 @@ def test_write_to_json(self): stats_time_to_first_response=stats_time_to_first_response, stats_time_to_close=stats_time_to_close, stats_time_to_answer=stats_time_to_answer, + stats_time_in_draft=stats_time_in_draft, stats_time_in_labels=stats_time_in_labels, num_issues_opened=num_issues_opened, num_issues_closed=num_issues_closed, @@ -154,6 +163,7 @@ def test_write_to_json_with_no_response(self): "med": {}, "90p": {}, } + stats_time_in_draft = None num_issues_opened = 2 num_issues_closed = 0 num_mentor_count = 5 @@ -162,14 +172,17 @@ def test_write_to_json_with_no_response(self): "average_time_to_first_response": "None", "average_time_to_close": "None", "average_time_to_answer": "None", + "average_time_in_draft": "None", "average_time_in_labels": {}, "median_time_to_first_response": "None", "median_time_to_close": "None", "median_time_to_answer": "None", + "median_time_in_draft": "None", "median_time_in_labels": {}, "90_percentile_time_to_first_response": "None", "90_percentile_time_to_close": "None", "90_percentile_time_to_answer": "None", + "90_percentile_time_in_draft": "None", "90_percentile_time_in_labels": {}, "num_items_opened": 2, "num_items_closed": 0, @@ -207,6 +220,7 @@ def test_write_to_json_with_no_response(self): stats_time_to_first_response=stats_time_to_first_response, stats_time_to_close=stats_time_to_close, stats_time_to_answer=stats_time_to_answer, + stats_time_in_draft=stats_time_in_draft, stats_time_in_labels=stats_time_in_labels, num_issues_opened=num_issues_opened, num_issues_closed=num_issues_closed, diff --git a/test_time_in_draft.py b/test_time_in_draft.py index 17ced108..f05ec267 100644 --- a/test_time_in_draft.py +++ b/test_time_in_draft.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock import pytz -from time_in_draft import measure_time_in_draft +from time_in_draft import get_stats_time_in_draft, measure_time_in_draft class TestMeasureTimeInDraft(unittest.TestCase): @@ -56,5 +56,57 @@ def test_time_in_draft_with_no_events(self): self.assertIsNone(result, "The result should be None when there are no events.") +class TestGetStatsTimeInDraft(unittest.TestCase): + """ + Unit tests for the get_stats_time_in_draft function. + """ + + def test_get_stats_time_in_draft_with_data(self): + """ + Test get_stats_time_in_draft with valid draft times. + """ + issues = [ + MagicMock(time_in_draft=timedelta(days=1)), + MagicMock(time_in_draft=timedelta(days=2)), + MagicMock(time_in_draft=timedelta(days=3)), + ] + + result = get_stats_time_in_draft(issues) + expected = { + "avg": timedelta(days=2), + "med": timedelta(days=2), + "90p": timedelta(days=2, seconds=69120), + } + + self.assertEqual( + result, expected, "The statistics for time in draft are incorrect." + ) + + def test_get_stats_time_in_draft_no_data(self): + """ + Test get_stats_time_in_draft with no draft times. + """ + issues = [ + MagicMock(time_in_draft=None), + MagicMock(time_in_draft=None), + ] + + result = get_stats_time_in_draft(issues) + self.assertIsNone( + result, "The result should be None when there are no draft times." + ) + + def test_get_stats_time_in_draft_empty_list(self): + """ + Test get_stats_time_in_draft with an empty list of issues. + """ + issues = [] + + result = get_stats_time_in_draft(issues) + self.assertIsNone( + result, "The result should be None when the list of issues is empty." + ) + + if __name__ == "__main__": unittest.main() diff --git a/time_in_draft.py b/time_in_draft.py index cef3a9bc..1650fd56 100644 --- a/time_in_draft.py +++ b/time_in_draft.py @@ -2,11 +2,13 @@ This module contains a function that measures the time a pull request has been in draft state. """ -from datetime import datetime -from typing import Union +from datetime import datetime, timedelta +from typing import List, Union import github3 +import numpy import pytz +from classes import IssueWithMetrics def measure_time_in_draft( @@ -42,3 +44,43 @@ def measure_time_in_draft( return None return None + + +def get_stats_time_in_draft( + issues_with_metrics: List[IssueWithMetrics], +) -> Union[dict[str, timedelta], None]: + """ + Calculate stats describing the time in draft for a list of issues. + """ + # Filter out issues with no time to answer + issues_with_time_to_draft = [ + issue for issue in issues_with_metrics if issue.time_in_draft is not None + ] + + # Calculate the total time in draft for all issues + draft_times = [] + if issues_with_time_to_draft: + for issue in issues_with_time_to_draft: + if issue.time_in_draft: + draft_times.append(issue.time_in_draft.total_seconds()) + + # Calculate stats describing time to answer + num_issues_with_time_in_draft = len(issues_with_time_to_draft) + if num_issues_with_time_in_draft > 0: + average_time_in_draft = numpy.round(numpy.average(draft_times)) + med_time_in_draft = numpy.round(numpy.median(draft_times)) + ninety_percentile_time_in_draft = numpy.round( + numpy.percentile(draft_times, 90, axis=0) + ) + else: + return None + + stats = { + "avg": timedelta(seconds=average_time_in_draft), + "med": timedelta(seconds=med_time_in_draft), + "90p": timedelta(seconds=ninety_percentile_time_in_draft), + } + + # Print the average time to answer converting seconds to a readable time format + print(f"Average time to answer: {timedelta(seconds=average_time_in_draft)}") + return stats From c035f140c4bd8b1885ff6031d40447a6905b3e70 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Wed, 23 Oct 2024 13:15:42 -0700 Subject: [PATCH 193/358] feat: add time in draft tracking to main and markdown_writer functions and tests --- issue_metrics.py | 3 +++ markdown_writer.py | 50 ++++++++++++++++++++++++++++++++--------- test_markdown_writer.py | 45 ++++++++++++++++++++++++++++--------- 3 files changed, 77 insertions(+), 21 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index 4d2fc961..12286545 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -244,6 +244,7 @@ def main(): # pragma: no cover average_time_to_first_response=None, average_time_to_close=None, average_time_to_answer=None, + average_time_in_draft=None, average_time_in_labels=None, num_issues_opened=None, num_issues_closed=None, @@ -268,6 +269,7 @@ def main(): # pragma: no cover average_time_to_first_response=None, average_time_to_close=None, average_time_to_answer=None, + average_time_in_draft=None, average_time_in_labels=None, num_issues_opened=None, num_issues_closed=None, @@ -329,6 +331,7 @@ def main(): # pragma: no cover average_time_to_first_response=stats_time_to_first_response, average_time_to_close=stats_time_to_close, average_time_to_answer=stats_time_to_answer, + average_time_in_draft=stats_time_in_draft, average_time_in_labels=stats_time_in_labels, num_issues_opened=num_issues_open, num_issues_closed=num_issues_closed, diff --git a/markdown_writer.py b/markdown_writer.py index e8cb5e17..bcebc7ea 100644 --- a/markdown_writer.py +++ b/markdown_writer.py @@ -71,6 +71,10 @@ def get_non_hidden_columns(labels) -> List[str]: if not hide_time_to_answer: columns.append("Time to answer") + enable_time_in_draft = env_vars.draft_pr_tracking + if enable_time_in_draft: + columns.append("Time in draft") + hide_label_metrics = env_vars.hide_label_metrics if not hide_label_metrics and labels: for label in labels: @@ -84,6 +88,7 @@ def write_to_markdown( average_time_to_first_response: Union[dict[str, timedelta], None], average_time_to_close: Union[dict[str, timedelta], None], average_time_to_answer: Union[dict[str, timedelta], None], + average_time_in_draft: Union[dict[str, timedelta], None], average_time_in_labels: Union[dict, None], num_issues_opened: Union[int, None], num_issues_closed: Union[int, None], @@ -104,6 +109,7 @@ def write_to_markdown( response for the issues. average_time_to_close (datetime.timedelta): The average time to close for the issues. average_time_to_answer (datetime.timedelta): The average time to answer the discussions. + average_time_in_draft (datetime.timedelta): The average time spent in draft for the issues. average_time_in_labels (dict): A dictionary containing the average time spent in each label. file (file object, optional): The file object to write to. If not provided, a file named "issue_metrics.md" will be created. @@ -112,9 +118,12 @@ def write_to_markdown( num_mentor_count (int): The number of very active commentors. labels (List[str]): A list of the labels that are used in the issues. search_query (str): The search query used to find the issues. - hide_label_metrics (bool): Represents whether the user has chosen to hide label metrics in the output - hide_items_closed_count (bool): Represents whether the user has chosen to hide the number of items closed - non_mentioning_links (bool): Represents whether links do not cause a notification in the desitnation repository + hide_label_metrics (bool): Represents whether the user has chosen to hide label + metrics in the output + hide_items_closed_count (bool): Represents whether the user has chosen to hide + the number of items closed + non_mentioning_links (bool): Represents whether links do not cause a notification + in the destination repository report_title (str): The title of the report output_file (str): The name of the file to write the report to @@ -131,7 +140,8 @@ def write_to_markdown( if not issues_with_metrics or len(issues_with_metrics) == 0: file.write("no issues found for the given search criteria\n\n") file.write( - "\n_This report was generated with the [Issue Metrics Action](https://github.com/github/issue-metrics)_\n" + "\n_This report was generated with the \ +[Issue Metrics Action](https://github.com/github/issue-metrics)_\n" ) if search_query: file.write(f"Search query used to find these items: `{search_query}`\n") @@ -143,6 +153,7 @@ def write_to_markdown( average_time_to_first_response, average_time_to_close, average_time_to_answer, + average_time_in_draft, average_time_in_labels, num_issues_opened, num_issues_closed, @@ -189,13 +200,16 @@ def write_to_markdown( file.write(f" {issue.time_to_close} |") if "Time to answer" in columns: file.write(f" {issue.time_to_answer} |") + if "Time in draft" in columns: + file.write(f" {issue.time_in_draft} |") if labels and issue.label_metrics: for label in labels: if f"Time spent in {label}" in columns: file.write(f" {issue.label_metrics[label]} |") file.write("\n") file.write( - "\n_This report was generated with the [Issue Metrics Action](https://github.com/github/issue-metrics)_\n" + "\n_This report was generated with the \ +[Issue Metrics Action](https://github.com/github/issue-metrics)_\n" ) if search_query: file.write(f"Search query used to find these items: `{search_query}`\n") @@ -208,6 +222,7 @@ def write_overall_metrics_tables( stats_time_to_first_response, stats_time_to_close, stats_time_to_answer, + average_time_in_draft, stats_time_in_labels, num_issues_opened, num_issues_closed, @@ -219,12 +234,15 @@ def write_overall_metrics_tables( hide_items_closed_count=False, ): """Write the overall metrics tables to the markdown file.""" - if ( - "Time to first response" in columns - or "Time to close" in columns - or "Time to answer" in columns - or (hide_label_metrics is False and len(labels) > 0) - ): + if any( + column in columns + for column in [ + "Time to first response", + "Time to close", + "Time to answer", + "Time in draft", + ] + ) or (hide_label_metrics is False and len(labels) > 0): file.write("| Metric | Average | Median | 90th percentile |\n") file.write("| --- | --- | --- | ---: |\n") if "Time to first response" in columns: @@ -257,6 +275,16 @@ def write_overall_metrics_tables( ) else: file.write("| Time to answer | None | None | None |\n") + if "Time in draft" in columns: + if average_time_in_draft is not None: + file.write( + f"| Time in draft " + f"| {average_time_in_draft['avg']} " + f"| {average_time_in_draft['med']} " + f"| {average_time_in_draft['90p']} |\n" + ) + else: + file.write("| Time in draft | None | None | None |\n") if labels and stats_time_in_labels: for label in labels: if ( diff --git a/test_markdown_writer.py b/test_markdown_writer.py index 8d69d355..7f69abdd 100644 --- a/test_markdown_writer.py +++ b/test_markdown_writer.py @@ -18,7 +18,11 @@ @patch.dict( os.environ, - {"SEARCH_QUERY": "is:open repo:user/repo", "GH_TOKEN": "test_token"}, + { + "SEARCH_QUERY": "is:open repo:user/repo", + "GH_TOKEN": "test_token", + "DRAFT_PR_TRACKING": "True", + }, ) class TestWriteToMarkdown(unittest.TestCase): """Test the write_to_markdown function.""" @@ -43,7 +47,8 @@ def test_write_to_markdown(self): time_to_first_response=timedelta(days=1), time_to_close=timedelta(days=2), time_to_answer=timedelta(days=3), - labels_metrics={"bug": timedelta(days=1)}, + time_in_draft=timedelta(days=1), + labels_metrics={"bug": timedelta(days=4)}, ), IssueWithMetrics( title="Issue 2\r", @@ -52,6 +57,7 @@ def test_write_to_markdown(self): time_to_first_response=timedelta(days=3), time_to_close=timedelta(days=4), time_to_answer=timedelta(days=5), + time_in_draft=timedelta(days=1), labels_metrics={"bug": timedelta(days=2)}, ), ] @@ -70,6 +76,11 @@ def test_write_to_markdown(self): "med": timedelta(days=4), "90p": timedelta(days=4), } + time_in_draft = { + "avg": timedelta(days=1), + "med": timedelta(days=1), + "90p": timedelta(days=1), + } time_in_labels = { "avg": {"bug": "1 day, 12:00:00"}, "med": {"bug": "1 day, 12:00:00"}, @@ -86,6 +97,7 @@ def test_write_to_markdown(self): average_time_to_first_response=time_to_first_response, average_time_to_close=time_to_close, average_time_to_answer=time_to_answer, + average_time_in_draft=time_in_draft, average_time_in_labels=time_in_labels, num_issues_opened=num_issues_opened, num_issues_closed=num_issues_closed, @@ -106,6 +118,7 @@ def test_write_to_markdown(self): "| Time to first response | 2 days, 0:00:00 | 2 days, 0:00:00 | 2 days, 0:00:00 |\n" "| Time to close | 3 days, 0:00:00 | 3 days, 0:00:00 | 3 days, 0:00:00 |\n" "| Time to answer | 4 days, 0:00:00 | 4 days, 0:00:00 | 4 days, 0:00:00 |\n" + "| Time in draft | 1 day, 0:00:00 | 1 day, 0:00:00 | 1 day, 0:00:00 |\n" "| Time spent in bug | 1 day, 12:00:00 | 1 day, 12:00:00 | 1 day, 12:00:00 |\n" "\n" "| Metric | Count |\n" @@ -115,12 +128,12 @@ def test_write_to_markdown(self): "| Number of most active mentors | 5 |\n" "| Total number of items created | 2 |\n\n" "| Title | URL | Author | Time to first response | Time to close |" - " Time to answer | Time spent in bug |\n" - "| --- | --- | --- | --- | --- | --- | --- |\n" + " Time to answer | Time in draft | Time spent in bug |\n" + "| --- | --- | --- | --- | --- | --- | --- | --- |\n" "| Issue 1 | https://github.com/user/repo/issues/1 | [alice](https://github.com/alice) | 1 day, 0:00:00 | " - "2 days, 0:00:00 | 3 days, 0:00:00 | 1 day, 0:00:00 |\n" + "2 days, 0:00:00 | 3 days, 0:00:00 | 1 day, 0:00:00 | 4 days, 0:00:00 |\n" "| Issue 2 | https://github.com/user/repo/issues/2 | [bob](https://github.com/bob) | 3 days, 0:00:00 | " - "4 days, 0:00:00 | 5 days, 0:00:00 | 2 days, 0:00:00 |\n\n" + "4 days, 0:00:00 | 5 days, 0:00:00 | 1 day, 0:00:00 | 2 days, 0:00:00 |\n\n" "_This report was generated with the [Issue Metrics Action](https://github.com/github/issue-metrics)_\n" "Search query used to find these items: `is:issue is:open label:bug`\n" ) @@ -145,6 +158,7 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): time_to_first_response=timedelta(days=1), time_to_close=timedelta(days=2), time_to_answer=timedelta(days=3), + time_in_draft=timedelta(days=1), labels_metrics={"bug": timedelta(days=1)}, ), IssueWithMetrics( @@ -172,6 +186,11 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): "med": timedelta(days=4), "90p": timedelta(days=4), } + average_time_in_draft = { + "avg": timedelta(days=1), + "med": timedelta(days=1), + "90p": timedelta(days=1), + } average_time_in_labels = { "avg": {"bug": "1 day, 12:00:00"}, "med": {"bug": "1 day, 12:00:00"}, @@ -188,6 +207,7 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): average_time_to_first_response=average_time_to_first_response, average_time_to_close=average_time_to_close, average_time_to_answer=average_time_to_answer, + average_time_in_draft=average_time_in_draft, average_time_in_labels=average_time_in_labels, num_issues_opened=num_issues_opened, num_issues_closed=num_issues_closed, @@ -207,6 +227,7 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): "| Time to first response | 2 days, 0:00:00 | 2 days, 0:00:00 | 2 days, 0:00:00 |\n" "| Time to close | 3 days, 0:00:00 | 3 days, 0:00:00 | 3 days, 0:00:00 |\n" "| Time to answer | 4 days, 0:00:00 | 4 days, 0:00:00 | 4 days, 0:00:00 |\n" + "| Time in draft | 1 day, 0:00:00 | 1 day, 0:00:00 | 1 day, 0:00:00 |\n" "| Time spent in bug | 1 day, 12:00:00 | 1 day, 12:00:00 | 1 day, 12:00:00 |\n" "\n" "| Metric | Count |\n" @@ -216,12 +237,12 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): "| Number of most active mentors | 5 |\n" "| Total number of items created | 2 |\n\n" "| Title | URL | Author | Time to first response | Time to close |" - " Time to answer | Time spent in bug |\n" - "| --- | --- | --- | --- | --- | --- | --- |\n" + " Time to answer | Time in draft | Time spent in bug |\n" + "| --- | --- | --- | --- | --- | --- | --- | --- |\n" "| Issue 1 | https://github.com/user/repo/issues/1 | [alice](https://github.com/alice) | 1 day, 0:00:00 | " - "2 days, 0:00:00 | 3 days, 0:00:00 | 1 day, 0:00:00 |\n" + "2 days, 0:00:00 | 3 days, 0:00:00 | 1 day, 0:00:00 | 1 day, 0:00:00 |\n" "| feat| Issue 2 | https://github.com/user/repo/issues/2 | [bob](https://github.com/bob) | 3 days, 0:00:00 | " - "4 days, 0:00:00 | 5 days, 0:00:00 | 2 days, 0:00:00 |\n\n" + "4 days, 0:00:00 | 5 days, 0:00:00 | None | 2 days, 0:00:00 |\n\n" "_This report was generated with the [Issue Metrics Action](https://github.com/github/issue-metrics)_\n" ) self.assertEqual(content, expected_content) @@ -240,6 +261,7 @@ def test_write_to_markdown_no_issues(self): None, None, None, + None, report_title="Issue Metrics", ) @@ -289,6 +311,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): time_to_first_response=timedelta(minutes=10), time_to_close=timedelta(days=1), time_to_answer=timedelta(hours=2), + time_in_draft=timedelta(days=1), labels_metrics={ "label1": timedelta(days=1), }, @@ -308,6 +331,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): average_time_to_first_response = timedelta(minutes=15) average_time_to_close = timedelta(days=1.5) average_time_to_answer = timedelta(hours=3) + average_time_in_draft = timedelta(days=1) average_time_in_labels = { "label1": timedelta(days=1), } @@ -322,6 +346,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): average_time_to_close=average_time_to_close, average_time_to_answer=average_time_to_answer, average_time_in_labels=average_time_in_labels, + average_time_in_draft=average_time_in_draft, num_issues_opened=num_issues_opened, num_issues_closed=num_issues_closed, num_mentor_count=num_mentor_count, From a116564036aab81e4136024245312e15a449411e Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Wed, 23 Oct 2024 13:45:04 -0700 Subject: [PATCH 194/358] fix: use issue attributes instead of events Signed-off-by: Zack Koppert --- test_time_in_draft.py | 22 +++++++++------------- time_in_draft.py | 22 ++++------------------ 2 files changed, 13 insertions(+), 31 deletions(-) diff --git a/test_time_in_draft.py b/test_time_in_draft.py index f05ec267..486cb9bd 100644 --- a/test_time_in_draft.py +++ b/test_time_in_draft.py @@ -18,14 +18,8 @@ def setUp(self): Setup common test data and mocks. """ self.issue = MagicMock() - self.issue.issue.events.return_value = [ - MagicMock( - event="created_at", created_at=datetime(2021, 1, 1, tzinfo=pytz.utc) - ), - MagicMock( - event="other_event", created_at=datetime(2021, 1, 2, tzinfo=pytz.utc) - ), - ] + self.issue.issue.created_at = datetime(2021, 1, 1, tzinfo=pytz.utc) + self.issue.issue.state = "open" def test_time_in_draft_with_ready_for_review(self): """ @@ -38,7 +32,7 @@ def test_time_in_draft_with_ready_for_review(self): def test_time_in_draft_without_ready_for_review(self): """ - Test measure_time_in_draft when ready_for_review_at is not provided. + Test measure_time_in_draft when ready_for_review_at is not provided and issue is still open. """ now = datetime(2021, 1, 4, tzinfo=pytz.utc) with unittest.mock.patch("time_in_draft.datetime") as mock_datetime: @@ -47,13 +41,15 @@ def test_time_in_draft_without_ready_for_review(self): expected = timedelta(days=3) self.assertEqual(result, expected, "The time in draft should be 3 days.") - def test_time_in_draft_with_no_events(self): + def test_time_in_draft_without_ready_for_review_and_closed(self): """ - Test measure_time_in_draft when there are no events. + Test measure_time_in_draft when ready_for_review_at is not provided and issue is closed. """ - self.issue.issue.events.return_value = [] + self.issue.issue.state = "closed" result = measure_time_in_draft(self.issue, None) - self.assertIsNone(result, "The result should be None when there are no events.") + self.assertIsNone( + result, "The result should be None when draft was never used." + ) class TestGetStatsTimeInDraft(unittest.TestCase): diff --git a/time_in_draft.py b/time_in_draft.py index 1650fd56..db6f5006 100644 --- a/time_in_draft.py +++ b/time_in_draft.py @@ -25,24 +25,10 @@ def measure_time_in_draft( returns: Union[datetime, None]: The time the pull request was in draft state. """ - events = issue.issue.events(number=50) - try: - pr_opened_at = None - for event in events: - if event.event == "created_at": - pr_opened_at = event.created_at - if pr_opened_at and ready_for_review_at: - return ready_for_review_at - pr_opened_at - if pr_opened_at and not ready_for_review_at: - return datetime.now(pytz.utc) - pr_opened_at - - except TypeError as e: - print( - f"An error occurred processing review events for {issue.issue.html_url}. \ -Perhaps issue contains a ghost user. {e}" - ) - return None - + if ready_for_review_at: + return ready_for_review_at - issue.issue.created_at + if issue.issue.state == "open": + return datetime.now(pytz.utc) - issue.issue.created_at return None From 4447e12600dba2d397e4a9c682951c2278d6d95e Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Wed, 23 Oct 2024 13:49:21 -0700 Subject: [PATCH 195/358] docs: add draft pr tracking to readme Signed-off-by: Zack Koppert --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d429178f..cc291aa2 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Feel free to inquire about its usage by creating an issue in this repository. | Time to Close | The period from creation to closure.\* | | Time to Answer (Discussions Only) | The time from creation to an answer. | | Time in Label | The duration from label application to removal, requires `LABELS_TO_MEASURE` env variable. | +| Time in Draft (PRs Only) | The duration from creation to the PR being marked as ready for review. | \*For pull requests, these metrics exclude the time the PR was in draft mode. @@ -151,6 +152,7 @@ This action can be configured to authenticate with GitHub App Installation or Pe | `HIDE_TIME_TO_ANSWER` | False | False | If set to `true`, the time to answer a discussion will not be displayed in the generated Markdown file. | | `HIDE_TIME_TO_CLOSE` | False | False | If set to `true`, the time to close will not be displayed in the generated Markdown file. | | `HIDE_TIME_TO_FIRST_RESPONSE` | False | False | If set to `true`, the time to first response will not be displayed in the generated Markdown file. | +| `DRAFT_PR_TRACKING` | False | False | If set to `true`, draft PRs will be included in the metrics as a new column and in the summary stats. | | `IGNORE_USERS` | False | False | A comma separated list of users to ignore when calculating metrics. (ie. `IGNORE_USERS: 'user1,user2'`). To ignore bots, append `[bot]` to the user (ie. `IGNORE_USERS: 'github-actions[bot]'`) Users in this list will also have their authored issues and pull requests removed from the Markdown table. | | `ENABLE_MENTOR_COUNT` | False | False | If set to 'TRUE' count number of comments users left on discussions, issues and PRs and display number of active mentors | | `MIN_MENTOR_COMMENTS` | False | 10 | Minimum number of comments to count as a mentor | From 4f0264e5665ae3760ebb5bcabcf04fab7513050b Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Wed, 23 Oct 2024 13:54:19 -0700 Subject: [PATCH 196/358] chore: format markdown Signed-off-by: Zack Koppert --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cc291aa2..603bcda5 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ This action can be configured to authenticate with GitHub App Installation or Pe | `HIDE_TIME_TO_ANSWER` | False | False | If set to `true`, the time to answer a discussion will not be displayed in the generated Markdown file. | | `HIDE_TIME_TO_CLOSE` | False | False | If set to `true`, the time to close will not be displayed in the generated Markdown file. | | `HIDE_TIME_TO_FIRST_RESPONSE` | False | False | If set to `true`, the time to first response will not be displayed in the generated Markdown file. | -| `DRAFT_PR_TRACKING` | False | False | If set to `true`, draft PRs will be included in the metrics as a new column and in the summary stats. | +| `DRAFT_PR_TRACKING` | False | False | If set to `true`, draft PRs will be included in the metrics as a new column and in the summary stats. | | `IGNORE_USERS` | False | False | A comma separated list of users to ignore when calculating metrics. (ie. `IGNORE_USERS: 'user1,user2'`). To ignore bots, append `[bot]` to the user (ie. `IGNORE_USERS: 'github-actions[bot]'`) Users in this list will also have their authored issues and pull requests removed from the Markdown table. | | `ENABLE_MENTOR_COUNT` | False | False | If set to 'TRUE' count number of comments users left on discussions, issues and PRs and display number of active mentors | | `MIN_MENTOR_COMMENTS` | False | 10 | Minimum number of comments to count as a mentor | From a1bab0538f91ea907176dc32c268c1f9d681dd24 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Wed, 23 Oct 2024 21:28:56 -0700 Subject: [PATCH 197/358] Update time_in_draft.py Co-authored-by: JM (Jason Meridth) --- time_in_draft.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/time_in_draft.py b/time_in_draft.py index db6f5006..34c30cc7 100644 --- a/time_in_draft.py +++ b/time_in_draft.py @@ -50,7 +50,7 @@ def get_stats_time_in_draft( if issue.time_in_draft: draft_times.append(issue.time_in_draft.total_seconds()) - # Calculate stats describing time to answer + # Calculate stats describing time in draft num_issues_with_time_in_draft = len(issues_with_time_to_draft) if num_issues_with_time_in_draft > 0: average_time_in_draft = numpy.round(numpy.average(draft_times)) From c564829aa0081924f11df2fb24d956e7b150421a Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Wed, 23 Oct 2024 21:29:07 -0700 Subject: [PATCH 198/358] Update time_in_draft.py Co-authored-by: JM (Jason Meridth) --- time_in_draft.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/time_in_draft.py b/time_in_draft.py index 34c30cc7..e96d3404 100644 --- a/time_in_draft.py +++ b/time_in_draft.py @@ -38,7 +38,7 @@ def get_stats_time_in_draft( """ Calculate stats describing the time in draft for a list of issues. """ - # Filter out issues with no time to answer + # Filter out issues with no time in draft issues_with_time_to_draft = [ issue for issue in issues_with_metrics if issue.time_in_draft is not None ] From fb362c05699e7f5f66211b43b637760f33ab8a84 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Wed, 23 Oct 2024 21:29:29 -0700 Subject: [PATCH 199/358] Update time_in_draft.py Co-authored-by: JM (Jason Meridth) --- time_in_draft.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/time_in_draft.py b/time_in_draft.py index e96d3404..59fe24a5 100644 --- a/time_in_draft.py +++ b/time_in_draft.py @@ -67,6 +67,6 @@ def get_stats_time_in_draft( "90p": timedelta(seconds=ninety_percentile_time_in_draft), } - # Print the average time to answer converting seconds to a readable time format - print(f"Average time to answer: {timedelta(seconds=average_time_in_draft)}") + # Print the average time in draft converting seconds to a readable time format + print(f"Average time in draft: {timedelta(seconds=average_time_in_draft)}") return stats From 8f8f5066e8d3ccc7a6e13d0b1087193005e9dcd9 Mon Sep 17 00:00:00 2001 From: ricardojdsilva87 Date: Mon, 28 Oct 2024 15:58:55 +0000 Subject: [PATCH 200/358] feat: support github enterprise api --- .env-example | 1 + auth.py | 48 +++++++++++++++++++++++++++++------------ config.py | 9 ++++++-- discussions.py | 5 +++-- issue_metrics.py | 20 ++++++++++------- markdown_writer.py | 14 ++++++++---- test_auth.py | 33 +++++++++++++++++++++------- test_config.py | 26 ++++++++++++++++++++++ test_discussions.py | 8 +++++-- test_markdown_writer.py | 1 + 10 files changed, 125 insertions(+), 40 deletions(-) diff --git a/.env-example b/.env-example index c7d8193f..ebc938d0 100644 --- a/.env-example +++ b/.env-example @@ -1,6 +1,7 @@ GH_APP_ID="" GH_APP_INSTALLATION_ID="" GH_APP_PRIVATE_KEY="" +GITHUB_APP_ENTERPRISE_ONLY="" GH_ENTERPRISE_URL = "" GH_TOKEN = "" HIDE_AUTHOR = "false" diff --git a/auth.py b/auth.py index 25a37cb3..53574196 100644 --- a/auth.py +++ b/auth.py @@ -1,58 +1,78 @@ -""" -This is the module that contains functions related to authenticating -to GitHub. -""" +"""This is the module that contains functions related to authenticating to GitHub with a personal access token.""" import github3 import requests def auth_to_github( - gh_app_id: str, - gh_app_installation_id: int, - gh_app_private_key_bytes: bytes, token: str, + gh_app_id: int | None, + gh_app_installation_id: int | None, + gh_app_private_key_bytes: bytes, ghe: str, + gh_app_enterprise_only: bool, ) -> github3.GitHub: """ Connect to GitHub.com or GitHub Enterprise, depending on env variables. + Args: + token (str): the GitHub personal access token + gh_app_id (int | None): the GitHub App ID + gh_app_installation_id (int | None): the GitHub App Installation ID + gh_app_private_key_bytes (bytes): the GitHub App Private Key + ghe (str): the GitHub Enterprise URL + gh_app_enterprise_only (bool): Set this to true if the GH APP is created + on GHE and needs to communicate with GHE api only + Returns: - github3.GitHub: A github api connection. + github3.GitHub: the GitHub connection object """ - if gh_app_id and gh_app_private_key_bytes and gh_app_installation_id: - gh = github3.github.GitHub() + if ghe and gh_app_enterprise_only: + gh = github3.github.GitHubEnterprise(url=ghe) + else: + gh = github3.github.GitHub() gh.login_as_app_installation( gh_app_private_key_bytes, gh_app_id, gh_app_installation_id ) github_connection = gh elif ghe and token: - github_connection = github3.github.GitHubEnterprise(ghe, token=token) + github_connection = github3.github.GitHubEnterprise(url=ghe, token=token) elif token: github_connection = github3.login(token=token) else: raise ValueError( - "GH_TOKEN or the set of [GH_APP_ID, GH_APP_INSTALLATION_ID, GH_APP_PRIVATE_KEY] environment variables are not set" + "GH_TOKEN or the set of [GH_APP_ID, GH_APP_INSTALLATION_ID, \ + GH_APP_PRIVATE_KEY] environment variables are not set" ) + if not github_connection: + raise ValueError("Unable to authenticate to GitHub") return github_connection # type: ignore def get_github_app_installation_token( - gh_app_id: str, gh_app_private_key_bytes: bytes, gh_app_installation_id: str + ghe: str, + gh_app_id: str, + gh_app_private_key_bytes: bytes, + gh_app_installation_id: str, ) -> str | None: """ Get a GitHub App Installation token. + API: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation + Args: + ghe (str): the GitHub Enterprise endpoint gh_app_id (str): the GitHub App ID gh_app_private_key_bytes (bytes): the GitHub App Private Key gh_app_installation_id (str): the GitHub App Installation ID + Returns: str: the GitHub App token """ jwt_headers = github3.apps.create_jwt_headers(gh_app_private_key_bytes, gh_app_id) - url = f"https://api.github.com/app/installations/{gh_app_installation_id}/access_tokens" + api_endpoint = f"{ghe}/api/v3" if ghe else "https://api.github.com" + url = f"{api_endpoint}/app/installations/{gh_app_installation_id}/access_tokens" try: response = requests.post(url, headers=jwt_headers, json=None, timeout=5) diff --git a/config.py b/config.py index 5de753f5..ba68ddd8 100644 --- a/config.py +++ b/config.py @@ -39,7 +39,7 @@ class EnvVars: hide_time_to_first_response (bool): If true, the time to first response metric is hidden in the output ignore_users (List[str]): List of usernames to ignore when calculating metrics - labels_to_measure (List[str]): List of labels to measure how much time the lable is applied + labels_to_measure (List[str]): List of labels to measure how much time the label is applied enable_mentor_count (bool): If set to TRUE, compute number of mentors min_mentor_comments (str): If set, defines the minimum number of comments for mentors max_comments_eval (str): If set, defines the maximum number of comments to look @@ -48,7 +48,7 @@ class EnvVars: involved commentors in search_query (str): Search query used to filter issues/prs/discussions on GitHub non_mentioning_links (bool): If set to TRUE, links do not cause a notification - in the desitnation repository + in the destination repository report_title (str): The title of the report output_file (str): The name of the file to write the report to rate_limit_bypass (bool): If set to TRUE, bypass the rate limit for the GitHub API @@ -61,6 +61,7 @@ def __init__( gh_app_id: int | None, gh_app_installation_id: int | None, gh_app_private_key_bytes: bytes, + gh_app_enterprise_only: bool, gh_token: str | None, ghe: str | None, hide_author: bool, @@ -85,6 +86,7 @@ def __init__( self.gh_app_id = gh_app_id self.gh_app_installation_id = gh_app_installation_id self.gh_app_private_key_bytes = gh_app_private_key_bytes + self.gh_app_enterprise_only = gh_app_enterprise_only self.gh_token = gh_token self.ghe = ghe self.ignore_users = ignore_user @@ -112,6 +114,7 @@ def __repr__(self): f"{self.gh_app_id}," f"{self.gh_app_installation_id}," f"{self.gh_app_private_key_bytes}," + f"{self.gh_app_enterprise_only}," f"{self.gh_token}," f"{self.ghe}," f"{self.hide_author}," @@ -186,6 +189,7 @@ def get_env_vars(test: bool = False) -> EnvVars: gh_app_id = get_int_env_var("GH_APP_ID") gh_app_private_key_bytes = os.environ.get("GH_APP_PRIVATE_KEY", "").encode("utf8") gh_app_installation_id = get_int_env_var("GH_APP_INSTALLATION_ID") + gh_app_enterprise_only = get_bool_env_var("GITHUB_APP_ENTERPRISE_ONLY") if gh_app_id and (not gh_app_private_key_bytes or not gh_app_installation_id): raise ValueError( @@ -235,6 +239,7 @@ def get_env_vars(test: bool = False) -> EnvVars: gh_app_id, gh_app_installation_id, gh_app_private_key_bytes, + gh_app_enterprise_only, gh_token, ghe, hide_author, diff --git a/discussions.py b/discussions.py index 8cc3d99f..c6700b40 100644 --- a/discussions.py +++ b/discussions.py @@ -10,7 +10,7 @@ import requests -def get_discussions(token: str, search_query: str): +def get_discussions(token: str, search_query: str, ghe: str): """Get a list of discussions in a GitHub repository that match the search query. Args: @@ -51,9 +51,10 @@ def get_discussions(token: str, search_query: str): variables = {"query": search_query} # Send the GraphQL request + api_endpoint = f"{ghe}/api/v3" if ghe else "https://api.github.com" headers = {"Authorization": f"Bearer {token}"} response = requests.post( - "https://api.github.com/graphql", + f"{api_endpoint}/graphql", json={"query": query, "variables": variables}, headers=headers, timeout=60, diff --git a/issue_metrics.py b/issue_metrics.py index 12286545..cdbaef99 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -192,23 +192,27 @@ def main(): # pragma: no cover output_file = env_vars.output_file rate_limit_bypass = env_vars.rate_limit_bypass + ghe = env_vars.ghe gh_app_id = env_vars.gh_app_id gh_app_installation_id = env_vars.gh_app_installation_id gh_app_private_key_bytes = env_vars.gh_app_private_key_bytes - - if not token and gh_app_id and gh_app_installation_id and gh_app_private_key_bytes: - token = get_github_app_installation_token( - gh_app_id, gh_app_private_key_bytes, gh_app_installation_id - ) + gh_app_enterprise_only = env_vars.gh_app_enterprise_only # Auth to GitHub.com github_connection = auth_to_github( + token, gh_app_id, gh_app_installation_id, gh_app_private_key_bytes, - token, - env_vars.ghe, + ghe, + gh_app_enterprise_only, ) + + if not token and gh_app_id and gh_app_installation_id and gh_app_private_key_bytes: + token = get_github_app_installation_token( + ghe, gh_app_id, gh_app_private_key_bytes, gh_app_installation_id + ) + enable_mentor_count = env_vars.enable_mentor_count min_mentor_count = int(env_vars.min_mentor_comments) max_comments_eval = int(env_vars.max_comments_eval) @@ -236,7 +240,7 @@ def main(): # pragma: no cover raise ValueError( "The search query for discussions cannot include labels to measure" ) - issues = get_discussions(token, search_query) + issues = get_discussions(token, search_query, ghe) if len(issues) <= 0: print("No discussions found") write_to_markdown( diff --git a/markdown_writer.py b/markdown_writer.py index bcebc7ea..ded4c113 100644 --- a/markdown_writer.py +++ b/markdown_writer.py @@ -100,6 +100,7 @@ def write_to_markdown( non_mentioning_links=False, report_title="", output_file="", + ghe="", ) -> None: """Write the issues with metrics to a markdown file. @@ -114,7 +115,7 @@ def write_to_markdown( file (file object, optional): The file object to write to. If not provided, a file named "issue_metrics.md" will be created. num_issues_opened (int): The Number of items that remain opened. - num_issues_closed (int): The number of issues that were closedi. + num_issues_closed (int): The number of issues that were closed. num_mentor_count (int): The number of very active commentors. labels (List[str]): A list of the labels that are used in the issues. search_query (str): The search query used to find the issues. @@ -126,6 +127,7 @@ def write_to_markdown( in the destination repository report_title (str): The title of the report output_file (str): The name of the file to write the report to + ghe (str): the GitHub Enterprise endpoint Returns: None. @@ -185,15 +187,19 @@ def write_to_markdown( # Replace any whitespace issue.title = issue.title.strip() + endpoint = ghe.removeprefix("https://") if ghe else "github.com" if non_mentioning_links: file.write( f"| {issue.title} | " - f"{issue.html_url.replace('https://github.com', 'https://www.github.com')} |" + f"{issue.html_url}".replace( + f"https://{endpoint}", f"https://www.{endpoint}" + ) + + " |" ) else: - file.write(f"| {issue.title} | " f"{issue.html_url} |") + file.write(f"| {issue.title} | {issue.html_url} |") if "Author" in columns: - file.write(f" [{issue.author}](https://github.com/{issue.author}) |") + file.write(f" [{issue.author}](https://{endpoint}/{issue.author}) |") if "Time to first response" in columns: file.write(f" {issue.time_to_first_response} |") if "Time to close" in columns: diff --git a/test_auth.py b/test_auth.py index 7057ff70..e2649386 100644 --- a/test_auth.py +++ b/test_auth.py @@ -25,17 +25,17 @@ def test_auth_to_github_with_github_app(self, mock_login): parameters provided. """ mock_login.return_value = MagicMock() - result = auth_to_github(12345, 678910, b"hello", "", "") + result = auth_to_github("", 12345, 678910, b"hello", "", False) - self.assertIsInstance(result, github3.github.GitHub) + self.assertIsInstance(result, github3.github.GitHub, False) def test_auth_to_github_with_token(self): """ Test the auth_to_github function when the token is provided. """ - result = auth_to_github(None, None, b"", "token", "") + result = auth_to_github("token", None, None, b"", "", False) - self.assertIsInstance(result, github3.github.GitHub) + self.assertIsInstance(result, github3.github.GitHub, False) def test_auth_to_github_without_authentication_information(self): """ @@ -43,15 +43,31 @@ def test_auth_to_github_without_authentication_information(self): Expect a ValueError to be raised. """ with self.assertRaises(ValueError): - auth_to_github(None, None, b"", "", "") + auth_to_github("", None, None, b"", "", False) def test_auth_to_github_with_ghe(self): """ Test the auth_to_github function when the GitHub Enterprise URL is provided. """ - result = auth_to_github(None, None, b"", "token", "https://github.example.com") + result = auth_to_github( + "token", None, None, b"", "https://github.example.com", False + ) + + self.assertIsInstance(result, github3.github.GitHubEnterprise, False) - self.assertIsInstance(result, github3.github.GitHubEnterprise) + @patch("github3.github.GitHubEnterprise") + def test_auth_to_github_with_ghe_and_ghe_app(self, mock_ghe): + """ + Test the auth_to_github function when the GitHub Enterprise URL \ + is provided and the app was created in GitHub Enterprise URL. + """ + mock = mock_ghe.return_value + mock.login_as_app_installation = MagicMock(return_value=True) + result = auth_to_github( + "", "123", "123", b"123", "https://github.example.com", True + ) + mock.login_as_app_installation.assert_called_once() + self.assertEqual(result, mock) @patch("github3.apps.create_jwt_headers", MagicMock(return_value="gh_token")) @patch("requests.post") @@ -64,9 +80,10 @@ def test_get_github_app_installation_token(self, mock_post): mock_response.raise_for_status.return_value = None mock_response.json.return_value = {"token": dummy_token} mock_post.return_value = mock_response + mock_ghe = "" result = get_github_app_installation_token( - b"gh_private_token", "gh_app_id", "gh_installation_id" + mock_ghe, b"gh_private_token", "gh_app_id", "gh_installation_id" ) self.assertEqual(result, dummy_token) diff --git a/test_config.py b/test_config.py index dab4e7d2..9197dd1f 100644 --- a/test_config.py +++ b/test_config.py @@ -119,6 +119,7 @@ def test_get_env_vars_with_github_app(self): gh_app_id=12345, gh_app_installation_id=678910, gh_app_private_key_bytes=b"hello", + gh_app_enterprise_only=False, gh_token="", ghe="", hide_author=False, @@ -171,6 +172,7 @@ def test_get_env_vars_with_token(self): gh_app_id=None, gh_app_installation_id=None, gh_app_private_key_bytes=b"", + gh_app_enterprise_only=False, gh_token=TOKEN, ghe="", hide_author=False, @@ -258,6 +260,7 @@ def test_get_env_vars_optional_values(self): gh_app_id=None, gh_app_installation_id=None, gh_app_private_key_bytes=b"", + gh_app_enterprise_only=False, gh_token=TOKEN, ghe="", hide_author=True, @@ -299,6 +302,7 @@ def test_get_env_vars_optionals_are_defaulted(self): gh_app_id=None, gh_app_installation_id=None, gh_app_private_key_bytes=b"", + gh_app_enterprise_only=False, gh_token="TOKEN", ghe="", hide_author=False, @@ -323,6 +327,28 @@ def test_get_env_vars_optionals_are_defaulted(self): result = get_env_vars(True) self.assertEqual(str(result), str(expected_result)) + @patch.dict( + os.environ, + { + "ORGANIZATION": "my_organization", + "GH_APP_ID": "12345", + "GH_APP_INSTALLATION_ID": "", + "GH_APP_PRIVATE_KEY": "", + "GH_TOKEN": "", + "SEARCH_QUERY": SEARCH_QUERY, + }, + clear=True, + ) + def test_get_env_vars_auth_with_github_app_installation_missing_inputs(self): + """Test that an error is raised there are missing inputs for the gh app""" + with self.assertRaises(ValueError) as context_manager: + get_env_vars(True) + the_exception = context_manager.exception + self.assertEqual( + str(the_exception), + "GH_APP_ID set and GH_APP_INSTALLATION_ID or GH_APP_PRIVATE_KEY variable not set", + ) + if __name__ == "__main__": unittest.main() diff --git a/test_discussions.py b/test_discussions.py index ffea4356..22b247fe 100644 --- a/test_discussions.py +++ b/test_discussions.py @@ -57,9 +57,12 @@ def test_get_discussions(self, mock_post): } mock_post.return_value.status_code = 200 mock_post.return_value.json.return_value = mock_response + mock_ghe = "" # Call the function with mock arguments - discussions = get_discussions("token", "repo:user/repo type:discussions query") + discussions = get_discussions( + "token", "repo:user/repo type:discussions query", mock_ghe + ) # Check that the function returns the expected discussions self.assertEqual(len(discussions), 2) @@ -75,7 +78,8 @@ def test_get_discussions_error(self, mock_post): """ # Mock a failed GraphQL response mock_post.return_value.status_code = 500 + mock_ghe = "" # Call the function with mock arguments and check that it raises an error with self.assertRaises(ValueError): - get_discussions("token", "repo:user/repo type:discussions query") + get_discussions("token", "repo:user/repo type:discussions query", mock_ghe) diff --git a/test_markdown_writer.py b/test_markdown_writer.py index 7f69abdd..25aec529 100644 --- a/test_markdown_writer.py +++ b/test_markdown_writer.py @@ -106,6 +106,7 @@ def test_write_to_markdown(self): search_query="is:issue is:open label:bug", report_title="Issue Metrics", output_file="issue_metrics.md", + ghe="", ) # Check that the function writes the correct markdown file From 65f3690f9856f793223f44a1c4c81462376f1622 Mon Sep 17 00:00:00 2001 From: ricardojdsilva87 Date: Mon, 28 Oct 2024 16:02:39 +0000 Subject: [PATCH 201/358] feat: update README --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 603bcda5..494708d3 100644 --- a/README.md +++ b/README.md @@ -128,11 +128,12 @@ This action can be configured to authenticate with GitHub App Installation or Pe ##### GitHub App Installation -| field | required | default | description | -| ------------------------ | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `GH_APP_ID` | True | `""` | GitHub Application ID. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. | -| `GH_APP_INSTALLATION_ID` | True | `""` | GitHub Application Installation ID. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. | -| `GH_APP_PRIVATE_KEY` | True | `""` | GitHub Application Private Key. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. | +| field | required | default | description | +| ---------------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `GH_APP_ID` | True | `""` | GitHub Application ID. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. | +| `GH_APP_INSTALLATION_ID` | True | `""` | GitHub Application Installation ID. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. | +| `GH_APP_PRIVATE_KEY` | True | `""` | GitHub Application Private Key. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. | +| `GITHUB_APP_ENTERPRISE_ONLY` | False | false | Set this input to `true` if your app is created in GHE and communicates with GHE. | ##### Personal Access Token (PAT) From 6081e47b59882a2d4c4449f419cc7273f3077325 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 17:18:26 +0000 Subject: [PATCH 202/358] chore(deps): bump mypy from 1.12.1 to 1.13.0 in the dependencies group Bumps the dependencies group with 1 update: [mypy](https://github.com/python/mypy). Updates `mypy` from 1.12.1 to 1.13.0 - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.12.1...v1.13.0) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 3a952a8f..8ca48bbe 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,6 +1,6 @@ black==24.10.0 flake8==7.1.1 -mypy==1.12.1 +mypy==1.13.0 mypy-extensions==1.0.0 pylint==3.3.1 pytest==8.3.3 From 24693370e2437700e7e5198db29b81cac284f8c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 17:41:05 +0000 Subject: [PATCH 203/358] chore(deps): bump the dependencies group with 3 updates Bumps the dependencies group with 3 updates: [actions/checkout](https://github.com/actions/checkout), [actions/setup-python](https://github.com/actions/setup-python) and [github/codeql-action](https://github.com/github/codeql-action). Updates `actions/checkout` from 4.2.1 to 4.2.2 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871...11bd71901bbe5b1630ceea73d27597364c9af683) Updates `actions/setup-python` from 5.2.0 to 5.3.0 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/f677139bbe7f9c59b41e40162b753c062f5d49a3...0b93645e9fea7318ecaed2b359559ac225c90a2b) Updates `github/codeql-action` from 3.26.13 to 3.27.0 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/f779452ac5af1c261dce0346a8f964149f49322b...662472033e021d55d94146f66f6058822b0b39fd) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/docker-image.yml | 2 +- .github/workflows/linter.yaml | 4 ++-- .github/workflows/major-version-updater.yml | 2 +- .github/workflows/python-package.yml | 4 ++-- .github/workflows/release.yml | 2 +- .github/workflows/scorecard.yml | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index a338d1b4..c0b29398 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -14,6 +14,6 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Build the Docker image run: docker build . --file Dockerfile --platform linux/amd64 --tag issue-metrics:"$(date +%s)" diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index bb49eefb..fd1fb21b 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -18,12 +18,12 @@ jobs: statuses: write steps: - name: Checkout Code - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # Full git history is needed to get a proper # list of changed files within `super-linter` fetch-depth: 0 - - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: python-version: "3.12" - name: Install dependencies diff --git a/.github/workflows/major-version-updater.yml b/.github/workflows/major-version-updater.yml index a4f5b5e4..e03b8764 100644 --- a/.github/workflows/major-version-updater.yml +++ b/.github/workflows/major-version-updater.yml @@ -15,7 +15,7 @@ jobs: contents: write steps: - name: Checkout Repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: version id: version diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 92f2fcd0..65016391 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -21,9 +21,9 @@ jobs: python-version: [3.11, 3.12] steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 21442eb8..5e3fe22f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,7 +60,7 @@ jobs: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Push Docker Image if: ${{ success() }} uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 18dea43c..98fb271d 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -25,7 +25,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.24.9 + uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.24.9 with: sarif_file: results.sarif From 11558bd0b8c1604b6562b5ddce0362794ba0e8ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:19:59 +0000 Subject: [PATCH 204/358] chore(deps): bump numpy from 2.1.2 to 2.1.3 in the dependencies group Bumps the dependencies group with 1 update: [numpy](https://github.com/numpy/numpy). Updates `numpy` from 2.1.2 to 2.1.3 - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.1.2...v2.1.3) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a99c6e0a..c04eb47d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ github3.py==4.0.1 -numpy==2.1.2 +numpy==2.1.3 python-dotenv==1.0.1 pytz==2024.2 requests==2.32.3 From ab22ad0a9600db133507376e73a7824fa4d521f9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:20:02 +0000 Subject: [PATCH 205/358] chore(deps): bump pytest-cov from 5.0.0 to 6.0.0 Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 5.0.0 to 6.0.0. - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v5.0.0...v6.0.0) --- updated-dependencies: - dependency-name: pytest-cov dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 8ca48bbe..42601d97 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -4,6 +4,6 @@ mypy==1.13.0 mypy-extensions==1.0.0 pylint==3.3.1 pytest==8.3.3 -pytest-cov==5.0.0 +pytest-cov==6.0.0 types-pytz==2024.2.0.20241003 types-requests==2.32.0.20241016 From d06df5871bb0567fd3c7ca72a9be2be5d38a9d1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:20:30 +0000 Subject: [PATCH 206/358] chore(deps): bump github/contributors in the dependencies group Bumps the dependencies group with 1 update: [github/contributors](https://github.com/github/contributors). Updates `github/contributors` from 1.5.0 to 1.5.1 - [Release notes](https://github.com/github/contributors/releases) - [Commits](https://github.com/github/contributors/compare/1286dc8d6904a9a7f735e28b7503be164fb7d4b9...90922d5748ecaf8417a3b7a0eedb4892c8fa1c44) --- updated-dependencies: - dependency-name: github/contributors dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/contributor_report.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/contributor_report.yaml b/.github/workflows/contributor_report.yaml index 5ffdb749..b8a3b69a 100644 --- a/.github/workflows/contributor_report.yaml +++ b/.github/workflows/contributor_report.yaml @@ -27,7 +27,7 @@ jobs: echo "END_DATE=$end_date" >> "$GITHUB_ENV" - name: Run contributor action - uses: github/contributors@1286dc8d6904a9a7f735e28b7503be164fb7d4b9 + uses: github/contributors@90922d5748ecaf8417a3b7a0eedb4892c8fa1c44 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} START_DATE: ${{ env.START_DATE }} From c93514e5882498ad04ae6f86b26f876c1527f790 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Mon, 4 Nov 2024 09:25:49 -0800 Subject: [PATCH 207/358] Add version comments to GitHub actions --- .github/workflows/contributor_report.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/contributor_report.yaml b/.github/workflows/contributor_report.yaml index b8a3b69a..d940168b 100644 --- a/.github/workflows/contributor_report.yaml +++ b/.github/workflows/contributor_report.yaml @@ -27,7 +27,7 @@ jobs: echo "END_DATE=$end_date" >> "$GITHUB_ENV" - name: Run contributor action - uses: github/contributors@90922d5748ecaf8417a3b7a0eedb4892c8fa1c44 + uses: github/contributors@90922d5748ecaf8417a3b7a0eedb4892c8fa1c44 # v1.5.1 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} START_DATE: ${{ env.START_DATE }} @@ -36,7 +36,7 @@ jobs: SPONSOR_INFO: "true" - name: Create issue - uses: peter-evans/create-issue-from-file@e8ef132d6df98ed982188e460ebb3b5d4ef3a9cd + uses: peter-evans/create-issue-from-file@e8ef132d6df98ed982188e460ebb3b5d4ef3a9cd # v5.0.1 with: title: Monthly contributor report token: ${{ secrets.GITHUB_TOKEN }} From 8197448b300a7ecd6ed430b8693a0596c9ed4461 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Mon, 4 Nov 2024 20:53:11 -0800 Subject: [PATCH 208/358] test: improve auth test coverage by adding missing and failed app install tests Signed-off-by: Zack Koppert --- test_auth.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test_auth.py b/test_auth.py index e2649386..791c6f5c 100644 --- a/test_auth.py +++ b/test_auth.py @@ -12,6 +12,7 @@ from unittest.mock import MagicMock, patch import github3 +import requests from auth import auth_to_github, get_github_app_installation_token @@ -87,3 +88,39 @@ def test_get_github_app_installation_token(self, mock_post): ) self.assertEqual(result, dummy_token) + + @patch("github3.apps.create_jwt_headers", MagicMock(return_value="gh_token")) + @patch("auth.requests.post") + def test_get_github_app_installation_token_request_failure(self, mock_post): + """ + Test the get_github_app_installation_token function returns None when the request fails. + """ + # Mock the post request to raise a RequestException + mock_post.side_effect = requests.exceptions.RequestException("Request failed") + + # Call the function with test data + result = get_github_app_installation_token( + ghe="https://api.github.com", + gh_app_id=12345, + gh_app_private_key_bytes=b"private_key", + gh_app_installation_id=678910, + ) + + # Assert that the result is None + self.assertIsNone(result) + + @patch("github3.login") + def test_auth_to_github_missing_credentials(self, mock_login): + """ + Test the auth_to_github function raises correct ValueError + when credentials are present but incorrect. + """ + mock_login.return_value = None + with self.assertRaises(ValueError) as context_manager: + auth_to_github("not_a_valid_token", "", "", b"", "", False) + + the_exception = context_manager.exception + self.assertEqual( + str(the_exception), + "Unable to authenticate to GitHub", + ) From d50bf917c6463c4688b13bf403fcb3f61e174d98 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Mon, 4 Nov 2024 21:07:48 -0800 Subject: [PATCH 209/358] chore: fix naming to match test case Signed-off-by: Zack Koppert --- test_auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_auth.py b/test_auth.py index 791c6f5c..9788df01 100644 --- a/test_auth.py +++ b/test_auth.py @@ -110,7 +110,7 @@ def test_get_github_app_installation_token_request_failure(self, mock_post): self.assertIsNone(result) @patch("github3.login") - def test_auth_to_github_missing_credentials(self, mock_login): + def test_auth_to_github_invalid_credentials(self, mock_login): """ Test the auth_to_github function raises correct ValueError when credentials are present but incorrect. From c7851ae9794ed363e7db25d88d505e9413162c88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 17:22:06 +0000 Subject: [PATCH 210/358] chore(deps): bump github/codeql-action in the dependencies group Bumps the dependencies group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 3.27.0 to 3.27.1 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/662472033e021d55d94146f66f6058822b0b39fd...4f3212b61783c3c68e8309a0f18a699764811cda) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 98fb271d..c4e94bb9 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.24.9 + uses: github/codeql-action/upload-sarif@4f3212b61783c3c68e8309a0f18a699764811cda # v3.24.9 with: sarif_file: results.sarif From 7b454f72e8f6db8d5608e1c6063e4573364b3e4c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 18:14:29 +0000 Subject: [PATCH 211/358] chore(deps): bump the dependencies group with 2 updates Bumps the dependencies group with 2 updates: [super-linter/super-linter](https://github.com/super-linter/super-linter) and [github/codeql-action](https://github.com/github/codeql-action). Updates `super-linter/super-linter` from 7.1.0 to 7.2.0 - [Release notes](https://github.com/super-linter/super-linter/releases) - [Changelog](https://github.com/super-linter/super-linter/blob/main/CHANGELOG.md) - [Commits](https://github.com/super-linter/super-linter/compare/b92721f792f381cedc002ecdbb9847a15ece5bb8...e1cb86b6e8d119f789513668b4b30bf17fe1efe4) Updates `github/codeql-action` from 3.27.1 to 3.27.4 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/4f3212b61783c3c68e8309a0f18a699764811cda...ea9e4e37992a54ee68a9622e985e60c8e8f12d9f) --- updated-dependencies: - dependency-name: super-linter/super-linter dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/linter.yaml | 2 +- .github/workflows/scorecard.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index fd1fb21b..f3ecf3ae 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -30,7 +30,7 @@ jobs: run: | pip install -r requirements.txt -r requirements-test.txt - name: Lint Code Base - uses: super-linter/super-linter@b92721f792f381cedc002ecdbb9847a15ece5bb8 # v7.1.0 + uses: super-linter/super-linter@e1cb86b6e8d119f789513668b4b30bf17fe1efe4 # v7.2.0 env: DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index c4e94bb9..a767cbc5 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@4f3212b61783c3c68e8309a0f18a699764811cda # v3.24.9 + uses: github/codeql-action/upload-sarif@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.24.9 with: sarif_file: results.sarif From 0123d8f6741dab23d613d126ecd4eae7718dc50b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 18:31:54 +0000 Subject: [PATCH 212/358] chore(deps): bump python from `751d8be` to `4efa69b` Bumps python from `751d8be` to `4efa69b`. --- updated-dependencies: - dependency-name: python dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index b7a32970..d944e59f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.13-slim@sha256:751d8bece269ba9e672b3f2226050e7e6fb3f3da3408b5dcb5d415a054fcb061 +FROM python:3.13-slim@sha256:4efa69bf17cfbd83a9942e60e2642335c3b397448e00410063a0421f9727c4c4 LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From 27fea39cec4f4bbe0d74c17f44a12815f29ff746 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Tue, 19 Nov 2024 12:08:58 -0600 Subject: [PATCH 213/358] fix: linting errors after super-linter upgrade - [x] group commands instead of instead of individual redirects - [x] double quote variable to prevent globbing and word splitting - [x] disable too-many-positional-arguments pylint linting Signed-off-by: jmeridth --- .github/linters/.python-lint | 1 + .github/workflows/major-version-updater.yml | 4 +--- .github/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/linters/.python-lint b/.github/linters/.python-lint index da0f17df..758f92c8 100644 --- a/.github/linters/.python-lint +++ b/.github/linters/.python-lint @@ -435,6 +435,7 @@ disable=bad-inline-option, too-many-function-args, too-many-branches, too-many-locals, + too-many-positional-arguments, too-many-statements, useless-suppression, use-symbolic-message-instead, diff --git a/.github/workflows/major-version-updater.yml b/.github/workflows/major-version-updater.yml index e03b8764..db09649c 100644 --- a/.github/workflows/major-version-updater.yml +++ b/.github/workflows/major-version-updater.yml @@ -23,9 +23,7 @@ jobs: tag=${GITHUB_REF/refs\/tags\//} version=${tag#v} major=${version%%.*} - echo "tag=${tag}" >> "$GITHUB_OUTPUT" - echo "version=${version}" >> "$GITHUB_OUTPUT" - echo "major=${major}" >> "$GITHUB_OUTPUT" + { echo "tag=${tag}"; echo "version=${version}"; echo "major=${major}"; } >> "$GITHUB_OUTPUT" - name: force update major tag run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5e3fe22f..a9594566 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,7 +42,7 @@ jobs: id: get_tag_name run: | short_tag=$(echo ${{ steps.release-drafter.outputs.tag_name }} | cut -d. -f1) - echo "SHORT_TAG=$short_tag" >> $GITHUB_OUTPUT + echo "SHORT_TAG=$short_tag" >> "$GITHUB_OUTPUT" create_action_images: needs: create_release runs-on: ubuntu-latest From 6dd89a234ffed3d32602740930b43a7b6e0b7710 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 18:00:39 +0000 Subject: [PATCH 214/358] chore(deps): bump github/codeql-action in the dependencies group Bumps the dependencies group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 3.27.4 to 3.27.5 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/ea9e4e37992a54ee68a9622e985e60c8e8f12d9f...f09c1c0a94de965c15400f5634aa42fac8fb8f88) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index a767cbc5..5cd3e659 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.24.9 + uses: github/codeql-action/upload-sarif@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.24.9 with: sarif_file: results.sarif From 46336657717a610434ebe8b23ce034f18b86c8c3 Mon Sep 17 00:00:00 2001 From: Bregwin Jogi Date: Tue, 26 Nov 2024 17:50:46 -0500 Subject: [PATCH 215/358] Update time in draft to account for edge cases and update tests --- issue_metrics.py | 3 +-- test_time_in_draft.py | 62 ++++++++++++++++++++++++++++++++++++------- time_in_draft.py | 31 ++++++++++++++-------- 3 files changed, 73 insertions(+), 23 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index cdbaef99..0fd3a195 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -125,8 +125,7 @@ def get_per_issue_metrics( ready_for_review_at = get_time_to_ready_for_review(issue, pull_request) if env_vars.draft_pr_tracking: issue_with_metrics.time_in_draft = measure_time_in_draft( - issue=issue, - ready_for_review_at=ready_for_review_at, + issue=issue ) if env_vars.hide_time_to_first_response is False: diff --git a/test_time_in_draft.py b/test_time_in_draft.py index 486cb9bd..ceea0b8f 100644 --- a/test_time_in_draft.py +++ b/test_time_in_draft.py @@ -18,15 +18,17 @@ def setUp(self): Setup common test data and mocks. """ self.issue = MagicMock() - self.issue.issue.created_at = datetime(2021, 1, 1, tzinfo=pytz.utc) self.issue.issue.state = "open" def test_time_in_draft_with_ready_for_review(self): """ - Test measure_time_in_draft when ready_for_review_at is provided. + Test measure_time_in_draft with one draft and review interval. """ - ready_for_review_at = datetime(2021, 1, 3, tzinfo=pytz.utc) - result = measure_time_in_draft(self.issue, ready_for_review_at) + self.issue.events.return_value = [ + MagicMock(event="converted_to_draft", created_at=datetime(2021, 1, 1, tzinfo=pytz.utc)), + MagicMock(event="ready_for_review", created_at=datetime(2021, 1, 3, tzinfo=pytz.utc)), + ] + result = measure_time_in_draft(self.issue) expected = timedelta(days=2) self.assertEqual(result, expected, "The time in draft should be 2 days.") @@ -34,22 +36,62 @@ def test_time_in_draft_without_ready_for_review(self): """ Test measure_time_in_draft when ready_for_review_at is not provided and issue is still open. """ + self.issue.events.return_value = [ + MagicMock(event="converted_to_draft", created_at=datetime(2021, 1, 1, tzinfo=pytz.utc)), + ] now = datetime(2021, 1, 4, tzinfo=pytz.utc) with unittest.mock.patch("time_in_draft.datetime") as mock_datetime: mock_datetime.now.return_value = now - result = measure_time_in_draft(self.issue, None) + result = measure_time_in_draft(self.issue) expected = timedelta(days=3) self.assertEqual(result, expected, "The time in draft should be 3 days.") + + def test_time_in_draft_multiple_intervals(self): + """ + Test measure_time_in_draft with multiple draft intervals. + """ + self.issue.events.return_value = [ + MagicMock(event="converted_to_draft", created_at=datetime(2021, 1, 1, tzinfo=pytz.utc)), + MagicMock(event="ready_for_review", created_at=datetime(2021, 1, 3, tzinfo=pytz.utc)), + MagicMock(event="converted_to_draft", created_at=datetime(2021, 1, 5, tzinfo=pytz.utc)), + MagicMock(event="ready_for_review", created_at=datetime(2021, 1, 7, tzinfo=pytz.utc)), + ] + result = measure_time_in_draft(self.issue) + expected = timedelta(days=4) + self.assertEqual(result, expected, "The total time in draft should be 4 days.") + + def test_time_in_draft_ongoing_draft(self): + """ + Test measure_time_in_draft with an ongoing draft interval. + """ + self.issue.events.return_value = [ + MagicMock(event="converted_to_draft", created_at=datetime(2021, 1, 1, tzinfo=pytz.utc)), + ] + with unittest.mock.patch("time_in_draft.datetime") as mock_datetime: + mock_datetime.now.return_value = datetime(2021, 1, 4, tzinfo=pytz.utc) + result = measure_time_in_draft(self.issue) + expected = timedelta(days=3) + self.assertEqual(result, expected, "The ongoing draft time should be 3 days.") + + def test_time_in_draft_no_draft_events(self): + """ + Test measure_time_in_draft with no draft-related events. + """ + self.issue.events.return_value = [] + result = measure_time_in_draft(self.issue) + self.assertIsNone(result, "The result should be None when there are no draft events.") + def test_time_in_draft_without_ready_for_review_and_closed(self): """ - Test measure_time_in_draft when ready_for_review_at is not provided and issue is closed. + Test measure_time_in_draft for a closed issue with an ongoing draft and ready_for_review_at is not provided. """ + self.issue.events.return_value = [ + MagicMock(event="converted_to_draft", created_at=datetime(2021, 1, 1, tzinfo=pytz.utc)), + ] self.issue.issue.state = "closed" - result = measure_time_in_draft(self.issue, None) - self.assertIsNone( - result, "The result should be None when draft was never used." - ) + result = measure_time_in_draft(self.issue) + self.assertIsNone(result, "The result should be None for a closed issue with an ongoing draft.") class TestGetStatsTimeInDraft(unittest.TestCase): diff --git a/time_in_draft.py b/time_in_draft.py index 59fe24a5..92441456 100644 --- a/time_in_draft.py +++ b/time_in_draft.py @@ -13,23 +13,32 @@ def measure_time_in_draft( issue: github3.issues.Issue, - ready_for_review_at: Union[datetime, None], -) -> Union[datetime, None]: - """If a pull request has had time in the draft state, return the amount of time it was in draft. +) -> Union[timedelta, None]: + """If a pull request has had time in the draft state, return the cumulative amount of time it was in draft. args: issue (github3.issues.Issue): A GitHub issue which has been pre-qualified as a pull request. - ready_for_review_at (datetime | None): The time the pull request was marked as - ready for review. returns: - Union[datetime, None]: The time the pull request was in draft state. + Union[timedelta, None]: Total time the pull request has spent in draft state. """ - if ready_for_review_at: - return ready_for_review_at - issue.issue.created_at - if issue.issue.state == "open": - return datetime.now(pytz.utc) - issue.issue.created_at - return None + events = issue.events() + draft_start = None + total_draft_time = timedelta(0) + + for event in events: + if event.event == "converted_to_draft": + draft_start = event.created_at + elif event.event == "ready_for_review" and draft_start: + # Calculate draft time for this interval + total_draft_time += event.created_at - draft_start + draft_start = None + + # If the PR is currently in draft state, calculate the time in draft up to now + if draft_start and issue.issue.state == "open": + total_draft_time += datetime.now(pytz.utc) - draft_start + + return total_draft_time if total_draft_time > timedelta(0) else None def get_stats_time_in_draft( From de48501de5e541e565dde97d86479c3870b4b4ff Mon Sep 17 00:00:00 2001 From: Bregwin Jogi Date: Tue, 26 Nov 2024 18:07:44 -0500 Subject: [PATCH 216/358] Fix lint issues --- test_time_in_draft.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test_time_in_draft.py b/test_time_in_draft.py index ceea0b8f..1cfa067d 100644 --- a/test_time_in_draft.py +++ b/test_time_in_draft.py @@ -46,7 +46,6 @@ def test_time_in_draft_without_ready_for_review(self): expected = timedelta(days=3) self.assertEqual(result, expected, "The time in draft should be 3 days.") - def test_time_in_draft_multiple_intervals(self): """ Test measure_time_in_draft with multiple draft intervals. From 6683123855c1930a433a99364410b154633f3c8f Mon Sep 17 00:00:00 2001 From: Joao Almeida Date: Wed, 27 Nov 2024 09:13:03 +0000 Subject: [PATCH 217/358] Remove versioning from API endpoint URL in discussions.py --- discussions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discussions.py b/discussions.py index c6700b40..9b160907 100644 --- a/discussions.py +++ b/discussions.py @@ -51,7 +51,7 @@ def get_discussions(token: str, search_query: str, ghe: str): variables = {"query": search_query} # Send the GraphQL request - api_endpoint = f"{ghe}/api/v3" if ghe else "https://api.github.com" + api_endpoint = f"{ghe}/api" if ghe else "https://api.github.com" headers = {"Authorization": f"Bearer {token}"} response = requests.post( f"{api_endpoint}/graphql", From 1b8f123cd3eba9b37f04e6975de7732269277a42 Mon Sep 17 00:00:00 2001 From: Joao Almeida Date: Wed, 27 Nov 2024 15:12:55 +0000 Subject: [PATCH 218/358] Refactor get_discussions function for pagination support Closes #44 --- discussions.py | 66 ++++++++++++------- test_discussions.py | 153 ++++++++++++++++++++++++++++++-------------- 2 files changed, 148 insertions(+), 71 deletions(-) diff --git a/discussions.py b/discussions.py index c6700b40..09ecdf6b 100644 --- a/discussions.py +++ b/discussions.py @@ -16,15 +16,15 @@ def get_discussions(token: str, search_query: str, ghe: str): Args: token (str): A personal access token for GitHub. search_query (str): The search query to filter discussions by. + ghe (str): GitHub Enterprise URL if applicable, or None for github.com. Returns: list: A list of discussions in the repository that match the search query. - """ - # Construct the GraphQL query + # Construct the GraphQL query with pagination query = """ - query($query: String!) { - search(query: $query, type: DISCUSSION, first: 100) { + query($query: String!, $cursor: String) { + search(query: $query, type: DISCUSSION, first: 100, after: $cursor) { edges { node { ... on Discussion { @@ -41,34 +41,56 @@ def get_discussions(token: str, search_query: str, ghe: str): } } } + pageInfo { + hasNextPage + endCursor + } } } """ # Remove the type:discussions filter from the search query search_query = search_query.replace("type:discussions ", "") - # Set the variables for the GraphQL query - variables = {"query": search_query} - # Send the GraphQL request - api_endpoint = f"{ghe}/api/v3" if ghe else "https://api.github.com" + api_endpoint = f"{ghe}/api" if ghe else "https://api.github.com" headers = {"Authorization": f"Bearer {token}"} - response = requests.post( - f"{api_endpoint}/graphql", - json={"query": query, "variables": variables}, - headers=headers, - timeout=60, - ) - # Check for errors in the GraphQL response - if response.status_code != 200 or "errors" in response.json(): - raise ValueError("GraphQL query failed") + discussions = [] + cursor = None - data = response.json()["data"] + while True: + # Set the variables for the GraphQL query + variables = {"query": search_query, "cursor": cursor} - # Extract the discussions from the GraphQL response - discussions = [] - for edge in data["search"]["edges"]: - discussions.append(edge["node"]) + # Send the GraphQL request + response = requests.post( + f"{api_endpoint}/graphql", + json={"query": query, "variables": variables}, + headers=headers, + timeout=60, + ) + + # Check for errors in the GraphQL response + if response.status_code != 200: + raise ValueError( + f"GraphQL query failed with status code {response.status_code}" + ) + + response_json = response.json() + if "errors" in response_json: + raise ValueError(f"GraphQL query failed: {response_json['errors']}") + + data = response_json["data"] + + # Extract the discussions from the current page + for edge in data["search"]["edges"]: + discussions.append(edge["node"]) + + # Check if there are more pages + page_info = data["search"]["pageInfo"] + if not page_info["hasNextPage"]: + break + + cursor = page_info["endCursor"] return discussions diff --git a/test_discussions.py b/test_discussions.py index 22b247fe..8145aad4 100644 --- a/test_discussions.py +++ b/test_discussions.py @@ -14,54 +14,49 @@ class TestGetDiscussions(unittest.TestCase): """A class to test the get_discussions function in the discussions module.""" - @patch("requests.post") - def test_get_discussions(self, mock_post): - """Test the get_discussions function with a successful GraphQL response. - - This test mocks a successful GraphQL response and checks that the - function returns the expected discussions. - - """ - # Mock the GraphQL response - mock_response = { + def _create_mock_response( + self, discussions, has_next_page=False, end_cursor="cursor123" + ): + """Helper method to create a mock GraphQL response.""" + return { "data": { "search": { - "edges": [ - { - "node": { - "title": "Discussion 1", - "url": "https://github.com/user/repo/discussions/1", - "createdAt": "2021-01-01T00:00:00Z", - "comments": { - "nodes": [{"createdAt": "2021-01-01T00:01:00Z"}] - }, - "answerChosenAt": None, - "closedAt": None, - } - }, - { - "node": { - "title": "Discussion 2", - "url": "https://github.com/user/repo/discussions/2", - "createdAt": "2021-01-02T00:00:00Z", - "comments": { - "nodes": [{"createdAt": "2021-01-02T00:01:00Z"}] - }, - "answerChosenAt": "2021-01-03T00:00:00Z", - "closedAt": "2021-01-04T00:00:00Z", - } - }, - ] + "edges": [{"node": discussion} for discussion in discussions], + "pageInfo": {"hasNextPage": has_next_page, "endCursor": end_cursor}, } } } + + @patch("requests.post") + def test_get_discussions_single_page(self, mock_post): + """Test the get_discussions function with a single page of results.""" + # Mock data for two discussions + mock_discussions = [ + { + "title": "Discussion 1", + "url": "https://github.com/user/repo/discussions/1", + "createdAt": "2021-01-01T00:00:00Z", + "comments": {"nodes": [{"createdAt": "2021-01-01T00:01:00Z"}]}, + "answerChosenAt": None, + "closedAt": None, + }, + { + "title": "Discussion 2", + "url": "https://github.com/user/repo/discussions/2", + "createdAt": "2021-01-02T00:00:00Z", + "comments": {"nodes": [{"createdAt": "2021-01-02T00:01:00Z"}]}, + "answerChosenAt": "2021-01-03T00:00:00Z", + "closedAt": "2021-01-04T00:00:00Z", + }, + ] + mock_post.return_value.status_code = 200 - mock_post.return_value.json.return_value = mock_response - mock_ghe = "" + mock_post.return_value.json.return_value = self._create_mock_response( + mock_discussions, has_next_page=False + ) - # Call the function with mock arguments discussions = get_discussions( - "token", "repo:user/repo type:discussions query", mock_ghe + "token", "repo:user/repo type:discussions query", "" ) # Check that the function returns the expected discussions @@ -69,17 +64,77 @@ def test_get_discussions(self, mock_post): self.assertEqual(discussions[0]["title"], "Discussion 1") self.assertEqual(discussions[1]["title"], "Discussion 2") + # Verify only one API call was made + self.assertEqual(mock_post.call_count, 1) + @patch("requests.post") - def test_get_discussions_error(self, mock_post): - """Test the get_discussions function with a failed GraphQL response. + def test_get_discussions_multiple_pages(self, mock_post): + """Test the get_discussions function with multiple pages of results.""" + # Mock data for pagination + page1_discussions = [ + { + "title": "Discussion 1", + "url": "https://github.com/user/repo/discussions/1", + "createdAt": "2021-01-01T00:00:00Z", + "comments": {"nodes": [{"createdAt": "2021-01-01T00:01:00Z"}]}, + "answerChosenAt": None, + "closedAt": None, + } + ] + + page2_discussions = [ + { + "title": "Discussion 2", + "url": "https://github.com/user/repo/discussions/2", + "createdAt": "2021-01-02T00:00:00Z", + "comments": {"nodes": [{"createdAt": "2021-01-02T00:01:00Z"}]}, + "answerChosenAt": None, + "closedAt": None, + } + ] - This test mocks a failed GraphQL response and checks that the function raises a ValueError. + # Configure mock to return different responses for each call + mock_post.return_value.status_code = 200 + mock_post.return_value.json.side_effect = [ + self._create_mock_response( + page1_discussions, has_next_page=True, end_cursor="cursor123" + ), + self._create_mock_response(page2_discussions, has_next_page=False), + ] + + discussions = get_discussions( + "token", "repo:user/repo type:discussions query", "" + ) - """ - # Mock a failed GraphQL response + # Check that all discussions were returned + self.assertEqual(len(discussions), 2) + self.assertEqual(discussions[0]["title"], "Discussion 1") + self.assertEqual(discussions[1]["title"], "Discussion 2") + + # Verify that two API calls were made + self.assertEqual(mock_post.call_count, 2) + + @patch("requests.post") + def test_get_discussions_error_status_code(self, mock_post): + """Test the get_discussions function with a failed HTTP response.""" mock_post.return_value.status_code = 500 - mock_ghe = "" - # Call the function with mock arguments and check that it raises an error - with self.assertRaises(ValueError): - get_discussions("token", "repo:user/repo type:discussions query", mock_ghe) + with self.assertRaises(ValueError) as context: + get_discussions("token", "repo:user/repo type:discussions query", "") + + self.assertIn( + "GraphQL query failed with status code 500", str(context.exception) + ) + + @patch("requests.post") + def test_get_discussions_graphql_error(self, mock_post): + """Test the get_discussions function with GraphQL errors in response.""" + mock_post.return_value.status_code = 200 + mock_post.return_value.json.return_value = { + "errors": [{"message": "GraphQL Error"}] + } + + with self.assertRaises(ValueError) as context: + get_discussions("token", "repo:user/repo type:discussions query", "") + + self.assertIn("GraphQL query failed:", str(context.exception)) From b1273e519e3b7ebd021a5bb5a97fbad7e2c6ae23 Mon Sep 17 00:00:00 2001 From: Joao Almeida Date: Wed, 27 Nov 2024 15:20:26 +0000 Subject: [PATCH 219/358] fix lint issues --- discussions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/discussions.py b/discussions.py index 23529794..b3ab1d0f 100644 --- a/discussions.py +++ b/discussions.py @@ -52,7 +52,6 @@ def get_discussions(token: str, search_query: str, ghe: str): # Remove the type:discussions filter from the search query search_query = search_query.replace("type:discussions ", "") - # Send the GraphQL request api_endpoint = f"{ghe}/api" if ghe else "https://api.github.com" headers = {"Authorization": f"Bearer {token}"} From 7429d773172d1311cb96c94d5c802dae655ab847 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Sun, 1 Dec 2024 09:12:39 -0800 Subject: [PATCH 220/358] chore: fix style by running black lint formatter Signed-off-by: Zack Koppert --- test_time_in_draft.py | 58 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/test_time_in_draft.py b/test_time_in_draft.py index 1cfa067d..24751477 100644 --- a/test_time_in_draft.py +++ b/test_time_in_draft.py @@ -25,8 +25,14 @@ def test_time_in_draft_with_ready_for_review(self): Test measure_time_in_draft with one draft and review interval. """ self.issue.events.return_value = [ - MagicMock(event="converted_to_draft", created_at=datetime(2021, 1, 1, tzinfo=pytz.utc)), - MagicMock(event="ready_for_review", created_at=datetime(2021, 1, 3, tzinfo=pytz.utc)), + MagicMock( + event="converted_to_draft", + created_at=datetime(2021, 1, 1, tzinfo=pytz.utc), + ), + MagicMock( + event="ready_for_review", + created_at=datetime(2021, 1, 3, tzinfo=pytz.utc), + ), ] result = measure_time_in_draft(self.issue) expected = timedelta(days=2) @@ -37,7 +43,10 @@ def test_time_in_draft_without_ready_for_review(self): Test measure_time_in_draft when ready_for_review_at is not provided and issue is still open. """ self.issue.events.return_value = [ - MagicMock(event="converted_to_draft", created_at=datetime(2021, 1, 1, tzinfo=pytz.utc)), + MagicMock( + event="converted_to_draft", + created_at=datetime(2021, 1, 1, tzinfo=pytz.utc), + ), ] now = datetime(2021, 1, 4, tzinfo=pytz.utc) with unittest.mock.patch("time_in_draft.datetime") as mock_datetime: @@ -51,10 +60,22 @@ def test_time_in_draft_multiple_intervals(self): Test measure_time_in_draft with multiple draft intervals. """ self.issue.events.return_value = [ - MagicMock(event="converted_to_draft", created_at=datetime(2021, 1, 1, tzinfo=pytz.utc)), - MagicMock(event="ready_for_review", created_at=datetime(2021, 1, 3, tzinfo=pytz.utc)), - MagicMock(event="converted_to_draft", created_at=datetime(2021, 1, 5, tzinfo=pytz.utc)), - MagicMock(event="ready_for_review", created_at=datetime(2021, 1, 7, tzinfo=pytz.utc)), + MagicMock( + event="converted_to_draft", + created_at=datetime(2021, 1, 1, tzinfo=pytz.utc), + ), + MagicMock( + event="ready_for_review", + created_at=datetime(2021, 1, 3, tzinfo=pytz.utc), + ), + MagicMock( + event="converted_to_draft", + created_at=datetime(2021, 1, 5, tzinfo=pytz.utc), + ), + MagicMock( + event="ready_for_review", + created_at=datetime(2021, 1, 7, tzinfo=pytz.utc), + ), ] result = measure_time_in_draft(self.issue) expected = timedelta(days=4) @@ -65,13 +86,18 @@ def test_time_in_draft_ongoing_draft(self): Test measure_time_in_draft with an ongoing draft interval. """ self.issue.events.return_value = [ - MagicMock(event="converted_to_draft", created_at=datetime(2021, 1, 1, tzinfo=pytz.utc)), + MagicMock( + event="converted_to_draft", + created_at=datetime(2021, 1, 1, tzinfo=pytz.utc), + ), ] with unittest.mock.patch("time_in_draft.datetime") as mock_datetime: mock_datetime.now.return_value = datetime(2021, 1, 4, tzinfo=pytz.utc) result = measure_time_in_draft(self.issue) expected = timedelta(days=3) - self.assertEqual(result, expected, "The ongoing draft time should be 3 days.") + self.assertEqual( + result, expected, "The ongoing draft time should be 3 days." + ) def test_time_in_draft_no_draft_events(self): """ @@ -79,18 +105,26 @@ def test_time_in_draft_no_draft_events(self): """ self.issue.events.return_value = [] result = measure_time_in_draft(self.issue) - self.assertIsNone(result, "The result should be None when there are no draft events.") + self.assertIsNone( + result, "The result should be None when there are no draft events." + ) def test_time_in_draft_without_ready_for_review_and_closed(self): """ Test measure_time_in_draft for a closed issue with an ongoing draft and ready_for_review_at is not provided. """ self.issue.events.return_value = [ - MagicMock(event="converted_to_draft", created_at=datetime(2021, 1, 1, tzinfo=pytz.utc)), + MagicMock( + event="converted_to_draft", + created_at=datetime(2021, 1, 1, tzinfo=pytz.utc), + ), ] self.issue.issue.state = "closed" result = measure_time_in_draft(self.issue) - self.assertIsNone(result, "The result should be None for a closed issue with an ongoing draft.") + self.assertIsNone( + result, + "The result should be None for a closed issue with an ongoing draft.", + ) class TestGetStatsTimeInDraft(unittest.TestCase): From e5a999a4e110f9ab40db8c2154a66e9b9143f482 Mon Sep 17 00:00:00 2001 From: snickerjp Date: Mon, 2 Dec 2024 19:08:10 +0900 Subject: [PATCH 221/358] Fix the GITHUB_ prefix was unusable --- docs/authenticating-with-github-app-installation.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/authenticating-with-github-app-installation.md b/docs/authenticating-with-github-app-installation.md index 3ba2e3f5..96d91f63 100644 --- a/docs/authenticating-with-github-app-installation.md +++ b/docs/authenticating-with-github-app-installation.md @@ -40,8 +40,8 @@ jobs: - name: Run issue-metrics tool uses: github/issue-metrics@v3 env: - GH_APP_ID: ${{ secrets.GITHUB_APP_ID }} - GH_APP_INSTALLATION_ID: ${{ secrets.GITHUB_APP_INSTALLATION_ID }} + GH_APP_ID: ${{ secrets.GH_APP_ID }} + GH_APP_INSTALLATION_ID: ${{ secrets.GH_APP_INSTALLATION_ID }} GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }} SEARCH_QUERY: 'repo:owner/repo is:issue created:${{ env.last_month }} -reason:"not planned"' @@ -50,13 +50,13 @@ jobs: teamMembers="$(gh api /orgs/ORG/teams/TEAM_SLUG/members | jq -r '.[].login' | paste -sd, -)" echo 'TEAM_MEMBERS='$teamMembers >> $GITHUB_ENV env: - GITHUB_TOKEN: ${{ secrets.CUSTOM_TOKEN }} + GH_TOKEN: ${{ secrets.CUSTOM_TOKEN }} - name: Create issue uses: peter-evans/create-issue-from-file@v4 with: title: Monthly issue metrics report - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.GH_TOKEN }} content-filepath: ./issue_metrics.md assignees: ${{ env.TEAM_MEMBERS }} ``` From 269f6ea7e69721aec6aacd4ccae464d16843441a Mon Sep 17 00:00:00 2001 From: snickerjp Date: Mon, 2 Dec 2024 19:44:40 +0900 Subject: [PATCH 222/358] Restored GITHUB_TOKEN --- docs/authenticating-with-github-app-installation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/authenticating-with-github-app-installation.md b/docs/authenticating-with-github-app-installation.md index 96d91f63..767d8588 100644 --- a/docs/authenticating-with-github-app-installation.md +++ b/docs/authenticating-with-github-app-installation.md @@ -50,13 +50,13 @@ jobs: teamMembers="$(gh api /orgs/ORG/teams/TEAM_SLUG/members | jq -r '.[].login' | paste -sd, -)" echo 'TEAM_MEMBERS='$teamMembers >> $GITHUB_ENV env: - GH_TOKEN: ${{ secrets.CUSTOM_TOKEN }} + GITHUB_TOKEN: ${{ secrets.CUSTOM_TOKEN }} - name: Create issue uses: peter-evans/create-issue-from-file@v4 with: title: Monthly issue metrics report - token: ${{ secrets.GH_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} content-filepath: ./issue_metrics.md assignees: ${{ env.TEAM_MEMBERS }} ``` From ce65e71971dbd8dfa7640dd3ffee7450d18d1624 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 18:01:53 +0000 Subject: [PATCH 223/358] chore(deps): bump the dependencies group with 2 updates Bumps the dependencies group with 2 updates: [pylint](https://github.com/pylint-dev/pylint) and [pytest](https://github.com/pytest-dev/pytest). Updates `pylint` from 3.3.1 to 3.3.2 - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v3.3.1...v3.3.2) Updates `pytest` from 8.3.3 to 8.3.4 - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.3.3...8.3.4) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index 42601d97..aba11f46 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,8 +2,8 @@ black==24.10.0 flake8==7.1.1 mypy==1.13.0 mypy-extensions==1.0.0 -pylint==3.3.1 -pytest==8.3.3 +pylint==3.3.2 +pytest==8.3.4 pytest-cov==6.0.0 types-pytz==2024.2.0.20241003 types-requests==2.32.0.20241016 From b19e9aae50c5832a5a131567ef4cfa05e974d033 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 18:13:38 +0000 Subject: [PATCH 224/358] chore(deps): bump docker/build-push-action in the dependencies group Bumps the dependencies group with 1 update: [docker/build-push-action](https://github.com/docker/build-push-action). Updates `docker/build-push-action` from 6.9.0 to 6.10.0 - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/4f58ea79222b3b9dc2c8bbdd6debcef730109a75...48aba3b46d1b1fec4febb7c5d0c644b249a11355) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a9594566..1e43d892 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,7 +63,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Push Docker Image if: ${{ success() }} - uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 + uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 with: context: . file: ./Dockerfile From a6c4f36513e2bbaf7baed9984cc9b696cc4e5bf0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 17:37:16 +0000 Subject: [PATCH 225/358] chore(deps): bump numpy from 2.1.3 to 2.2.0 in the dependencies group Bumps the dependencies group with 1 update: [numpy](https://github.com/numpy/numpy). Updates `numpy` from 2.1.3 to 2.2.0 - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.1.3...v2.2.0) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c04eb47d..4136fb76 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ github3.py==4.0.1 -numpy==2.1.3 +numpy==2.2.0 python-dotenv==1.0.1 pytz==2024.2 requests==2.32.3 From d628ada577b3d0c4146b000779efecb73a559e61 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 17:47:03 +0000 Subject: [PATCH 226/358] chore(deps): bump python from `4efa69b` to `f41a75c` Bumps python from `4efa69b` to `f41a75c`. --- updated-dependencies: - dependency-name: python dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d944e59f..cade1463 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.13-slim@sha256:4efa69bf17cfbd83a9942e60e2642335c3b397448e00410063a0421f9727c4c4 +FROM python:3.13-slim@sha256:f41a75c9cee9391c09e0139f7b49d4b1fbb119944ec740ecce4040626dc07bed LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From c3aa79efefcc8f90cd98fb55d388a38b035740bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 18:01:04 +0000 Subject: [PATCH 227/358] chore(deps): bump github/codeql-action in the dependencies group Bumps the dependencies group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 3.27.5 to 3.27.6 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/f09c1c0a94de965c15400f5634aa42fac8fb8f88...aa578102511db1f4524ed59b8cc2bae4f6e88195) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 5cd3e659..1a8ff40a 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.24.9 + uses: github/codeql-action/upload-sarif@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.24.9 with: sarif_file: results.sarif From a35d638f3eb8a57a5ed8ec0ee70241a129b3edf0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:35:07 +0000 Subject: [PATCH 228/358] chore(deps): bump the dependencies group with 3 updates Bumps the dependencies group with 3 updates: [super-linter/super-linter](https://github.com/super-linter/super-linter), [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) and [github/codeql-action](https://github.com/github/codeql-action). Updates `super-linter/super-linter` from 7.2.0 to 7.2.1 - [Release notes](https://github.com/super-linter/super-linter/releases) - [Changelog](https://github.com/super-linter/super-linter/blob/main/CHANGELOG.md) - [Commits](https://github.com/super-linter/super-linter/compare/e1cb86b6e8d119f789513668b4b30bf17fe1efe4...85f7611e0f7b53c8573cca84aa0ed4344f6f6a4d) Updates `docker/setup-buildx-action` from 3.7.1 to 3.8.0 - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/c47758b77c9736f4b2ef4073d4d51994fabfe349...6524bf65af31da8d45b59e8c27de4bd072b392f5) Updates `github/codeql-action` from 3.27.6 to 3.27.9 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/aa578102511db1f4524ed59b8cc2bae4f6e88195...df409f7d9260372bd5f19e5b04e83cb3c43714ae) --- updated-dependencies: - dependency-name: super-linter/super-linter dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: docker/setup-buildx-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/linter.yaml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/scorecard.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index f3ecf3ae..401b4877 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -30,7 +30,7 @@ jobs: run: | pip install -r requirements.txt -r requirements-test.txt - name: Lint Code Base - uses: super-linter/super-linter@e1cb86b6e8d119f789513668b4b30bf17fe1efe4 # v7.2.0 + uses: super-linter/super-linter@85f7611e0f7b53c8573cca84aa0ed4344f6f6a4d # v7.2.1 env: DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1e43d892..c20c1d52 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,7 +53,7 @@ jobs: IMAGE_NAME: github/issue_metrics # different than repo name (underscore instead of dash) steps: - name: Set up Docker Buildx - uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 + uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 - name: Log in to the Container registry uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 with: diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 1a8ff40a..7e0603ed 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.24.9 + uses: github/codeql-action/upload-sarif@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.24.9 with: sarif_file: results.sarif From 9002f32f626b36b8a1e3547e24eede400a0ae4b9 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Sun, 22 Dec 2024 08:25:55 -0600 Subject: [PATCH 229/358] fix: use immutable semantic versions for actions that allow it Signed-off-by: jmeridth --- .github/workflows/docker-image.yml | 2 +- .github/workflows/linter.yaml | 4 ++-- .github/workflows/major-version-updater.yml | 2 +- .github/workflows/python-package.yml | 4 ++-- .github/workflows/release.yml | 2 +- .github/workflows/scorecard.yml | 4 ++-- .github/workflows/stale.yaml | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index c0b29398..3c2f8ec2 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -14,6 +14,6 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@v4.2.2 - name: Build the Docker image run: docker build . --file Dockerfile --platform linux/amd64 --tag issue-metrics:"$(date +%s)" diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index 401b4877..7572271a 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -18,12 +18,12 @@ jobs: statuses: write steps: - name: Checkout Code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@v4.2.2 with: # Full git history is needed to get a proper # list of changed files within `super-linter` fetch-depth: 0 - - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 + - uses: actions/setup-python@v5.3.0 with: python-version: "3.12" - name: Install dependencies diff --git a/.github/workflows/major-version-updater.yml b/.github/workflows/major-version-updater.yml index db09649c..67f46dd3 100644 --- a/.github/workflows/major-version-updater.yml +++ b/.github/workflows/major-version-updater.yml @@ -15,7 +15,7 @@ jobs: contents: write steps: - name: Checkout Repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@v4.2.2 - name: version id: version diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 65016391..146b24eb 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -21,9 +21,9 @@ jobs: python-version: [3.11, 3.12] steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@v4.2.2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 + uses: actions/setup-python@v5.3.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c20c1d52..675f3e57 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,7 +60,7 @@ jobs: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@v4.2.2 - name: Push Docker Image if: ${{ success() }} uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 7e0603ed..f5077bf6 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -25,7 +25,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@v4.2.2 with: persist-credentials: false @@ -36,7 +36,7 @@ jobs: results_format: sarif publish_results: true - name: "Upload artifact" - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@v4.5.0 with: name: SARIF file path: results.sarif diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 94485211..2d8c416e 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -11,7 +11,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e + - uses: actions/stale@v9.0.0 with: stale-issue-message: "This issue is stale because it has been open 21 days with no activity. Remove stale label or comment or this will be closed in 14 days." close-issue-message: "This issue was closed because it has been stalled for 35 days with no activity." From 714bd8f39e0e4f4a1abb63f254d0e8c123ec2cd0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 17:38:00 +0000 Subject: [PATCH 230/358] chore(deps): bump github/codeql-action in the dependencies group Bumps the dependencies group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 3.27.9 to 3.28.0 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/df409f7d9260372bd5f19e5b04e83cb3c43714ae...48ab28a6f5dbc2a99bf1e0131198dd8f1df78169) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index f5077bf6..90caa396 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.24.9 + uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.24.9 with: sarif_file: results.sarif From 9d94014286549785b94e920494e08301cba0fe97 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 17:48:43 +0000 Subject: [PATCH 231/358] chore(deps): bump types-pytz from 2024.2.0.20241003 to 2024.2.0.20241221 Bumps [types-pytz](https://github.com/python/typeshed) from 2024.2.0.20241003 to 2024.2.0.20241221. - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-pytz dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index aba11f46..8295100c 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -5,5 +5,5 @@ mypy-extensions==1.0.0 pylint==3.3.2 pytest==8.3.4 pytest-cov==6.0.0 -types-pytz==2024.2.0.20241003 +types-pytz==2024.2.0.20241221 types-requests==2.32.0.20241016 From 2fbf988c78fc8a69cb4c2d7e33fce514cfe16083 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 17:52:35 +0000 Subject: [PATCH 232/358] chore(deps): bump the dependencies group with 2 updates Bumps the dependencies group with 2 updates: [numpy](https://github.com/numpy/numpy) and [mypy](https://github.com/python/mypy). Updates `numpy` from 2.2.0 to 2.2.1 - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.2.0...v2.2.1) Updates `mypy` from 1.13.0 to 1.14.0 - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.13.0...v1.14.0) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index 8295100c..4cce130f 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,6 +1,6 @@ black==24.10.0 flake8==7.1.1 -mypy==1.13.0 +mypy==1.14.0 mypy-extensions==1.0.0 pylint==3.3.2 pytest==8.3.4 diff --git a/requirements.txt b/requirements.txt index 4136fb76..4627a487 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ github3.py==4.0.1 -numpy==2.2.0 +numpy==2.2.1 python-dotenv==1.0.1 pytz==2024.2 requests==2.32.3 From dfefd3e89141ac25e843d4935755adb85024a0c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Dec 2024 17:17:56 +0000 Subject: [PATCH 233/358] chore(deps): bump python from `f41a75c` to `1127090` Bumps python from `f41a75c` to `1127090`. --- updated-dependencies: - dependency-name: python dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index cade1463..7f783b00 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.13-slim@sha256:f41a75c9cee9391c09e0139f7b49d4b1fbb119944ec740ecce4040626dc07bed +FROM python:3.13-slim@sha256:1127090f9fff0b8e7c3a1367855ef8a3299472d2c9ed122948a576c39addeaf1 LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From 7d3aa48728687253c70084c6c0fef783debe25fb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Dec 2024 17:41:41 +0000 Subject: [PATCH 234/358] chore(deps): bump the dependencies group with 2 updates Bumps the dependencies group with 2 updates: [mypy](https://github.com/python/mypy) and [pylint](https://github.com/pylint-dev/pylint). Updates `mypy` from 1.14.0 to 1.14.1 - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.14.0...v1.14.1) Updates `pylint` from 3.3.2 to 3.3.3 - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v3.3.2...v3.3.3) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: pylint dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index 4cce130f..da947c52 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,8 +1,8 @@ black==24.10.0 flake8==7.1.1 -mypy==1.14.0 +mypy==1.14.1 mypy-extensions==1.0.0 -pylint==3.3.2 +pylint==3.3.3 pytest==8.3.4 pytest-cov==6.0.0 types-pytz==2024.2.0.20241221 From 3caca4622da943d2e4c9a85682779b3ee032687b Mon Sep 17 00:00:00 2001 From: jmeridth Date: Sun, 5 Jan 2025 12:32:51 -0600 Subject: [PATCH 235/358] feat: move to reusable workflows - [x] auto-labeler - add major,minor,patch labels to auto-labeler - [x] pr-title - [x] release - [x] update release-drafter template to use $OWNER/$REPOSITORY https://github.com/jmeridth/reusable-workflows Signed-off-by: jmeridth --- .github/release-drafter.yml | 5 +- .github/workflows/auto-labeler.yml | 16 +-- .github/workflows/major-version-updater.yml | 31 ------ .github/workflows/pr-title.yml | 29 +----- .github/workflows/release.yml | 108 ++++++-------------- 5 files changed, 46 insertions(+), 143 deletions(-) delete mode 100644 .github/workflows/major-version-updater.yml diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 2ba0ceee..fb2ec4ba 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -5,7 +5,7 @@ template: | # Changelog $CHANGES - See details of [all code changes](https://github.com/github/issue-metrics/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION) since previous release + See details of [all code changes](https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION) since previous release categories: - title: "🚀 Features" @@ -32,14 +32,17 @@ version-resolver: major: labels: - "breaking" + - "major" minor: labels: - "enhancement" - "fix" + - "minor" patch: labels: - "documentation" - "maintenance" + - "patch" default: patch autolabeler: - label: "automation" diff --git a/.github/workflows/auto-labeler.yml b/.github/workflows/auto-labeler.yml index 9fe305e1..a684390b 100644 --- a/.github/workflows/auto-labeler.yml +++ b/.github/workflows/auto-labeler.yml @@ -1,24 +1,18 @@ --- name: Auto Labeler - on: # pull_request_target event is required for autolabeler to support all PRs including forks pull_request_target: types: [opened, reopened, edited, synchronize] - permissions: contents: read - jobs: main: permissions: contents: write pull-requests: write - name: Auto label pull requests - runs-on: ubuntu-latest - steps: - - uses: release-drafter/release-drafter@3f0f87098bd6b5c5b9a36d49c41d998ea58f9348 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - config-name: release-drafter.yml + uses: jmeridth/reusable-workflows/.github/workflows/auto-labeler.yaml@1406afbf7a795f706f04644059cecbb3b2f0c1a0 + with: + config-name: release-drafter.yml + secrets: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/major-version-updater.yml b/.github/workflows/major-version-updater.yml deleted file mode 100644 index 67f46dd3..00000000 --- a/.github/workflows/major-version-updater.yml +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: Major Version Updater -# Whenever a new release is made, push a major version tag -on: - release: - types: [published] - -permissions: - contents: read - -jobs: - update-major-version-tag: - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout Repo - uses: actions/checkout@v4.2.2 - - - name: version - id: version - run: | - tag=${GITHUB_REF/refs\/tags\//} - version=${tag#v} - major=${version%%.*} - { echo "tag=${tag}"; echo "version=${version}"; echo "major=${major}"; } >> "$GITHUB_OUTPUT" - - - name: force update major tag - run: | - git tag v${{ steps.version.outputs.major }} - git push origin refs/tags/v${{ steps.version.outputs.major }} -f diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index 08a66253..94453183 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -1,38 +1,17 @@ ## Reference: https://github.com/amannn/action-semantic-pull-request --- name: "Lint PR Title" - on: pull_request_target: types: [opened, reopened, edited, synchronize] - permissions: contents: read - jobs: main: permissions: + contents: read pull-requests: read statuses: write - name: Validate PR title - runs-on: ubuntu-latest - steps: - - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - # Configure which types are allowed (newline-delimited). - # From: https://github.com/commitizen/conventional-commit-types/blob/master/index.json - # listing all below - types: | - build - chore - ci - docs - feat - fix - perf - refactor - revert - style - test + uses: jmeridth/reusable-workflows/.github/workflows/pr-title.yaml@1406afbf7a795f706f04644059cecbb3b2f0c1a0 + secrets: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 675f3e57..404961ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,92 +1,50 @@ --- name: Release - on: workflow_dispatch: pull_request_target: types: [closed] branches: [main] - permissions: contents: read - jobs: - create_release: - # release if - # manual deployment OR - # merged to main and labelled with release labels - if: | - (github.event_name == 'workflow_dispatch') || - (github.event.pull_request.merged == true && - (contains(github.event.pull_request.labels.*.name, 'breaking') || - contains(github.event.pull_request.labels.*.name, 'feature') || - contains(github.event.pull_request.labels.*.name, 'vuln') || - contains(github.event.pull_request.labels.*.name, 'release'))) - outputs: - full-tag: ${{ steps.release-drafter.outputs.tag_name }} - short-tag: ${{ steps.get_tag_name.outputs.SHORT_TAG }} - body: ${{ steps.release-drafter.outputs.body }} - runs-on: ubuntu-latest + release: permissions: contents: write pull-requests: read - steps: - - uses: release-drafter/release-drafter@3f0f87098bd6b5c5b9a36d49c41d998ea58f9348 - id: release-drafter - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - config-name: release-drafter.yml - publish: true - - name: Get the short tag - id: get_tag_name - run: | - short_tag=$(echo ${{ steps.release-drafter.outputs.tag_name }} | cut -d. -f1) - echo "SHORT_TAG=$short_tag" >> "$GITHUB_OUTPUT" - create_action_images: - needs: create_release - runs-on: ubuntu-latest + uses: jmeridth/reusable-workflows/.github/workflows/release.yaml@53a9c808122ffaae9af948f72139fb4bd44ab74c + with: + publish: true + release-config-name: release-drafter.yaml + secrets: + github-token: ${{ secrets.GITHUB_TOKEN }} + release_image: + needs: release permissions: + contents: write + discussions: write packages: write - env: - REGISTRY: ghcr.io - IMAGE_NAME: github/issue_metrics # different than repo name (underscore instead of dash) - steps: - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 - - name: Log in to the Container registry - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/checkout@v4.2.2 - - name: Push Docker Image - if: ${{ success() }} - uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 - with: - context: . - file: ./Dockerfile - push: true - tags: | - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.create_release.outputs.full-tag }} - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.create_release.outputs.short-tag }} - platforms: linux/amd64 - provenance: false - sbom: false - create_discussion: - needs: create_release - runs-on: ubuntu-latest + pull-requests: read + uses: jmeridth/reusable-workflows/.github/workflows/release-image.yaml@53a9c808122ffaae9af948f72139fb4bd44ab74c + with: + image-name: jmeridth/reusable-workflows + full-tag: ${{ needs.release.outputs.full-tag }} + short-tag: ${{ needs.release.outputs.short-tag }} + secrets: + github-token: ${{ secrets.GITHUB_TOKEN }} + image-registry: ghcr.io + image-registry-username: ${{ github.actor }} + image-registry-password: ${{ secrets.GITHUB_TOKEN }} + release_discussion: + needs: release permissions: + contents: read discussions: write - steps: - - name: Create an announcement discussion for release - uses: abirismyname/create-discussion@6e6ef67e5eeb042343ef8b3d8d0f5d545cbdf024 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - title: ${{ needs.create_release.outputs.full-tag }} - body: ${{ needs.create_release.outputs.body }} - repository-id: ${{ secrets.RELEASE_DISCUSSION_REPOSITORY_ID }} - category-id: ${{ secrets.RELEASE_DISCUSSION_CATEGORY_ID }} + uses: jmeridth/reusable-workflows/.github/workflows/release-discussion.yaml@53a9c808122ffaae9af948f72139fb4bd44ab74c + with: + full-tag: ${{ needs.release.outputs.full-tag }} + body: ${{ needs.release.outputs.body }} + secrets: + github-token: ${{ secrets.GITHUB_TOKEN }} + discussion-repository-id: ${{ secrets.RELEASE_DISCUSSION_REPOSITORY_ID }} + discussion-category-id: ${{ secrets.RELEASE_DISCUSSION_CATEGORY_ID }} From 19acca943094ccc2f43b092c659eca4e82084e8e Mon Sep 17 00:00:00 2001 From: jmeridth Date: Sun, 5 Jan 2025 14:09:21 -0600 Subject: [PATCH 236/358] fix: release config name Signed-off-by: jmeridth --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 404961ed..18e574e2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: uses: jmeridth/reusable-workflows/.github/workflows/release.yaml@53a9c808122ffaae9af948f72139fb4bd44ab74c with: publish: true - release-config-name: release-drafter.yaml + release-config-name: release-drafter.yml secrets: github-token: ${{ secrets.GITHUB_TOKEN }} release_image: From 0e38ed8257ee1aac2898b532c1bf809ca8f9feb7 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Sun, 5 Jan 2025 14:51:51 -0600 Subject: [PATCH 237/358] fix: use correct image name for release Signed-off-by: jmeridth --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 18e574e2..4c9d1302 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: pull-requests: read uses: jmeridth/reusable-workflows/.github/workflows/release-image.yaml@53a9c808122ffaae9af948f72139fb4bd44ab74c with: - image-name: jmeridth/reusable-workflows + image-name: ${{ github.repository_owner }}/issue_metrics full-tag: ${{ needs.release.outputs.full-tag }} short-tag: ${{ needs.release.outputs.short-tag }} secrets: From 41fc5bbb9a725accf60b09f4841331bc7dd88fb1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 17:15:08 +0000 Subject: [PATCH 238/358] chore(deps): bump github/contributors in the dependencies group Bumps the dependencies group with 1 update: [github/contributors](https://github.com/github/contributors). Updates `github/contributors` from 1.5.1 to 1.5.3 - [Release notes](https://github.com/github/contributors/releases) - [Commits](https://github.com/github/contributors/compare/90922d5748ecaf8417a3b7a0eedb4892c8fa1c44...695ea9d3f1c31f6ff67ab7d6a964a15f8ef9fa04) --- updated-dependencies: - dependency-name: github/contributors dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/contributor_report.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/contributor_report.yaml b/.github/workflows/contributor_report.yaml index d940168b..f3c5041e 100644 --- a/.github/workflows/contributor_report.yaml +++ b/.github/workflows/contributor_report.yaml @@ -27,7 +27,7 @@ jobs: echo "END_DATE=$end_date" >> "$GITHUB_ENV" - name: Run contributor action - uses: github/contributors@90922d5748ecaf8417a3b7a0eedb4892c8fa1c44 # v1.5.1 + uses: github/contributors@695ea9d3f1c31f6ff67ab7d6a964a15f8ef9fa04 # v1.5.3 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} START_DATE: ${{ env.START_DATE }} From d3da543ee33a09d36c0a9de10c28987694d2e593 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 17:20:28 +0000 Subject: [PATCH 239/358] chore(deps): bump the dependencies group with 2 updates Bumps the dependencies group with 2 updates: [actions/upload-artifact](https://github.com/actions/upload-artifact) and [github/codeql-action](https://github.com/github/codeql-action). Updates `actions/upload-artifact` from 4.5.0 to 4.6.0 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4.5.0...v4.6.0) Updates `github/codeql-action` from 3.28.0 to 3.28.1 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/48ab28a6f5dbc2a99bf1e0131198dd8f1df78169...b6a472f63d85b9c78a3ac5e89422239fc15e9b3c) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/scorecard.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 90caa396..3b5e9024 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -36,12 +36,12 @@ jobs: results_format: sarif publish_results: true - name: "Upload artifact" - uses: actions/upload-artifact@v4.5.0 + uses: actions/upload-artifact@v4.6.0 with: name: SARIF file path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.24.9 + uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.24.9 with: sarif_file: results.sarif From 4abf3833dc17bbfcf0793fcd3102b233a4ca2858 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Fri, 17 Jan 2025 07:40:01 -0600 Subject: [PATCH 240/358] chore: update workflows after move jmeridth/reusable-workflows -> github/ospo-reusable-workflows - [x] updte CODEOWNERS also Signed-off-by: jmeridth --- .github/CODEOWNERS | 2 +- .github/workflows/auto-labeler.yml | 2 +- .github/workflows/pr-title.yml | 2 +- .github/workflows/release.yml | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cf1f49b2..a506e1eb 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @jmeridth @sutterj @zkoppert +* @github/ospo-github-actions diff --git a/.github/workflows/auto-labeler.yml b/.github/workflows/auto-labeler.yml index a684390b..7e419547 100644 --- a/.github/workflows/auto-labeler.yml +++ b/.github/workflows/auto-labeler.yml @@ -11,7 +11,7 @@ jobs: permissions: contents: write pull-requests: write - uses: jmeridth/reusable-workflows/.github/workflows/auto-labeler.yaml@1406afbf7a795f706f04644059cecbb3b2f0c1a0 + uses: github/ospo-reusable-workflows/.github/workflows/auto-labeler.yaml@1406afbf7a795f706f04644059cecbb3b2f0c1a0 with: config-name: release-drafter.yml secrets: diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index 94453183..70f19748 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -12,6 +12,6 @@ jobs: contents: read pull-requests: read statuses: write - uses: jmeridth/reusable-workflows/.github/workflows/pr-title.yaml@1406afbf7a795f706f04644059cecbb3b2f0c1a0 + uses: github/ospo-reusable-workflows/.github/workflows/pr-title.yaml@1406afbf7a795f706f04644059cecbb3b2f0c1a0 secrets: github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4c9d1302..f551ecef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: permissions: contents: write pull-requests: read - uses: jmeridth/reusable-workflows/.github/workflows/release.yaml@53a9c808122ffaae9af948f72139fb4bd44ab74c + uses: github/ospo-reusable-workflows/.github/workflows/release.yaml@53a9c808122ffaae9af948f72139fb4bd44ab74c with: publish: true release-config-name: release-drafter.yml @@ -25,7 +25,7 @@ jobs: discussions: write packages: write pull-requests: read - uses: jmeridth/reusable-workflows/.github/workflows/release-image.yaml@53a9c808122ffaae9af948f72139fb4bd44ab74c + uses: github/ospo-reusable-workflows/.github/workflows/release-image.yaml@53a9c808122ffaae9af948f72139fb4bd44ab74c with: image-name: ${{ github.repository_owner }}/issue_metrics full-tag: ${{ needs.release.outputs.full-tag }} @@ -40,7 +40,7 @@ jobs: permissions: contents: read discussions: write - uses: jmeridth/reusable-workflows/.github/workflows/release-discussion.yaml@53a9c808122ffaae9af948f72139fb4bd44ab74c + uses: github/ospo-reusable-workflows/.github/workflows/release-discussion.yaml@53a9c808122ffaae9af948f72139fb4bd44ab74c with: full-tag: ${{ needs.release.outputs.full-tag }} body: ${{ needs.release.outputs.body }} From 97c0e47d3ff5bc8f6b86f178330e11e6a54e7664 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2025 17:18:15 +0000 Subject: [PATCH 241/358] chore(deps): bump python from `1127090` to `23a81be` Bumps python from `1127090` to `23a81be`. --- updated-dependencies: - dependency-name: python dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 7f783b00..fd95e82a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.13-slim@sha256:1127090f9fff0b8e7c3a1367855ef8a3299472d2c9ed122948a576c39addeaf1 +FROM python:3.13-slim@sha256:23a81be7b258c8f516f7a60e80943cace4350deb8204cf107c7993e343610d47 LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From 795203bb43e1b4c22e20538f3d97f8ae1a41c454 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2025 17:29:19 +0000 Subject: [PATCH 242/358] chore(deps): bump numpy from 2.2.1 to 2.2.2 in the dependencies group Bumps the dependencies group with 1 update: [numpy](https://github.com/numpy/numpy). Updates `numpy` from 2.2.1 to 2.2.2 - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.2.1...v2.2.2) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4627a487..8af040dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ github3.py==4.0.1 -numpy==2.2.1 +numpy==2.2.2 python-dotenv==1.0.1 pytz==2024.2 requests==2.32.3 From 4eb445ccb905c9379d9980c611db929c2121e037 Mon Sep 17 00:00:00 2001 From: Chris McIntosh Date: Tue, 21 Jan 2025 16:07:27 -0600 Subject: [PATCH 243/358] Adding 'Created_At' column, defaulting to false --- README.md | 1 + classes.py | 2 ++ config.py | 5 +++++ issue_metrics.py | 5 +++++ markdown_writer.py | 6 ++++++ test_config.py | 4 ++++ 6 files changed, 23 insertions(+) diff --git a/README.md b/README.md index 494708d3..956c79bf 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,7 @@ This action can be configured to authenticate with GitHub App Installation or Pe | `HIDE_TIME_TO_ANSWER` | False | False | If set to `true`, the time to answer a discussion will not be displayed in the generated Markdown file. | | `HIDE_TIME_TO_CLOSE` | False | False | If set to `true`, the time to close will not be displayed in the generated Markdown file. | | `HIDE_TIME_TO_FIRST_RESPONSE` | False | False | If set to `true`, the time to first response will not be displayed in the generated Markdown file. | +| `HIDE_CREATED_AT` | False | True | If set to `true`, the creation timestmap will not be displayed in the generated Markdown file. | | `DRAFT_PR_TRACKING` | False | False | If set to `true`, draft PRs will be included in the metrics as a new column and in the summary stats. | | `IGNORE_USERS` | False | False | A comma separated list of users to ignore when calculating metrics. (ie. `IGNORE_USERS: 'user1,user2'`). To ignore bots, append `[bot]` to the user (ie. `IGNORE_USERS: 'github-actions[bot]'`) Users in this list will also have their authored issues and pull requests removed from the Markdown table. | | `ENABLE_MENTOR_COUNT` | False | False | If set to 'TRUE' count number of comments users left on discussions, issues and PRs and display number of active mentors | diff --git a/classes.py b/classes.py index 7428d31e..c0bf37a8 100644 --- a/classes.py +++ b/classes.py @@ -37,6 +37,7 @@ def __init__( time_in_draft=None, labels_metrics=None, mentor_activity=None, + created_at=None, ): self.title = title self.html_url = html_url @@ -47,3 +48,4 @@ def __init__( self.time_in_draft = time_in_draft self.label_metrics = labels_metrics self.mentor_activity = mentor_activity + self.created_at = created_at diff --git a/config.py b/config.py index ba68ddd8..38a7353c 100644 --- a/config.py +++ b/config.py @@ -70,6 +70,7 @@ def __init__( hide_time_to_answer: bool, hide_time_to_close: bool, hide_time_to_first_response: bool, + hide_created_at: bool, ignore_user: List[str], labels_to_measure: List[str], enable_mentor_count: bool, @@ -97,6 +98,7 @@ def __init__( self.hide_time_to_answer = hide_time_to_answer self.hide_time_to_close = hide_time_to_close self.hide_time_to_first_response = hide_time_to_first_response + self.hide_created_at = hide_created_at self.enable_mentor_count = enable_mentor_count self.min_mentor_comments = min_mentor_comments self.max_comments_eval = max_comments_eval @@ -123,6 +125,7 @@ def __repr__(self): f"{self.hide_time_to_answer}," f"{self.hide_time_to_close}," f"{self.hide_time_to_first_response}," + f"{self.hide_created_at}," f"{self.ignore_users}," f"{self.labels_to_measure}," f"{self.enable_mentor_count}," @@ -229,6 +232,7 @@ def get_env_vars(test: bool = False) -> EnvVars: hide_time_to_answer = get_bool_env_var("HIDE_TIME_TO_ANSWER", False) hide_time_to_close = get_bool_env_var("HIDE_TIME_TO_CLOSE", False) hide_time_to_first_response = get_bool_env_var("HIDE_TIME_TO_FIRST_RESPONSE", False) + hide_created_at = get_bool_env_var("HIDE_CREATED_AT", True) enable_mentor_count = get_bool_env_var("ENABLE_MENTOR_COUNT", False) min_mentor_comments = os.getenv("MIN_MENTOR_COMMENTS", "10") max_comments_eval = os.getenv("MAX_COMMENTS_EVAL", "20") @@ -248,6 +252,7 @@ def get_env_vars(test: bool = False) -> EnvVars: hide_time_to_answer, hide_time_to_close, hide_time_to_first_response, + hide_created_at, ignore_users_list, labels_to_measure_list, enable_mentor_count, diff --git a/issue_metrics.py b/issue_metrics.py index 0fd3a195..75cc3bf9 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -108,6 +108,8 @@ def get_per_issue_metrics( ) else: num_issues_open += 1 + if env_vars.hide_created_at is False: + issue_with_metrics.created_at = issue.created_at else: if ignore_users and issue.user["login"] in ignore_users: # type: ignore continue @@ -159,6 +161,9 @@ def get_per_issue_metrics( ) elif issue.state == "open": # type: ignore num_issues_open += 1 + if env_vars.hide_created_at is False: + issue_with_metrics.created_at = issue.created_at + issues_with_metrics.append(issue_with_metrics) return issues_with_metrics, num_issues_open, num_issues_closed diff --git a/markdown_writer.py b/markdown_writer.py index ded4c113..934c01fa 100644 --- a/markdown_writer.py +++ b/markdown_writer.py @@ -79,6 +79,9 @@ def get_non_hidden_columns(labels) -> List[str]: if not hide_label_metrics and labels: for label in labels: columns.append(f"Time spent in {label}") + hide_created_at = env_vars.hide_created_at + if not hide_created_at: + columns.append(f'Created At') return columns @@ -212,6 +215,8 @@ def write_to_markdown( for label in labels: if f"Time spent in {label}" in columns: file.write(f" {issue.label_metrics[label]} |") + if "Created At" in columns: + file.write(f" {issue.created_at} |") file.write("\n") file.write( "\n_This report was generated with the \ @@ -303,6 +308,7 @@ def write_overall_metrics_tables( f"| {stats_time_in_labels['med'][label]} " f"| {stats_time_in_labels['90p'][label]} |\n" ) + file.write("\n") # Write count stats to a separate table file.write("| Metric | Count |\n") diff --git a/test_config.py b/test_config.py index 9197dd1f..7a41ad10 100644 --- a/test_config.py +++ b/test_config.py @@ -128,6 +128,7 @@ def test_get_env_vars_with_github_app(self): hide_time_to_answer=False, hide_time_to_close=False, hide_time_to_first_response=False, + hide_created_at=True, ignore_user=[], labels_to_measure=[], enable_mentor_count=False, @@ -181,6 +182,7 @@ def test_get_env_vars_with_token(self): hide_time_to_answer=False, hide_time_to_close=False, hide_time_to_first_response=False, + hide_created_at=True, ignore_user=[], labels_to_measure=[], enable_mentor_count=False, @@ -269,6 +271,7 @@ def test_get_env_vars_optional_values(self): hide_time_to_answer=True, hide_time_to_close=True, hide_time_to_first_response=True, + hide_created_at=True, ignore_user=[], labels_to_measure=["waiting-for-review", "waiting-for-manager"], enable_mentor_count=False, @@ -311,6 +314,7 @@ def test_get_env_vars_optionals_are_defaulted(self): hide_time_to_answer=False, hide_time_to_close=False, hide_time_to_first_response=False, + hide_created_at=True, ignore_user=[], labels_to_measure=[], enable_mentor_count=False, From 3b13de5d02e17779c0763a09de0e9ece256ec165 Mon Sep 17 00:00:00 2001 From: Chris McIntosh Date: Sat, 25 Jan 2025 08:59:31 -0600 Subject: [PATCH 244/358] Update issue_metrics.py Co-authored-by: JM (Jason Meridth) Signed-off-by: Chris McIntosh --- issue_metrics.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/issue_metrics.py b/issue_metrics.py index 75cc3bf9..7aa2fa46 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -163,7 +163,8 @@ def get_per_issue_metrics( num_issues_open += 1 if env_vars.hide_created_at is False: issue_with_metrics.created_at = issue.created_at - +if not env_vars.hide_created_at: + issue_with_metrics.created_at = issue["createdAt"] issues_with_metrics.append(issue_with_metrics) return issues_with_metrics, num_issues_open, num_issues_closed From a6570c35d9c8a7c296cf48fc1d5014263fce5ae7 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Sat, 25 Jan 2025 18:26:40 -0600 Subject: [PATCH 245/358] fix: linting - [x] bad indenting - [x] bad code still present - [x] docs - [x] bad f string, single vs double quotes also - [x] line too long, ignoring Signed-off-by: jmeridth --- auth.py | 2 +- classes.py | 2 +- issue_metrics.py | 8 ++------ markdown_writer.py | 2 +- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/auth.py b/auth.py index 53574196..aa95f0d7 100644 --- a/auth.py +++ b/auth.py @@ -59,7 +59,7 @@ def get_github_app_installation_token( ) -> str | None: """ Get a GitHub App Installation token. - API: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation + API: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation # noqa: E501 Args: ghe (str): the GitHub Enterprise endpoint diff --git a/classes.py b/classes.py index c0bf37a8..414ab828 100644 --- a/classes.py +++ b/classes.py @@ -21,7 +21,7 @@ class IssueWithMetrics: time_in_draft (timedelta, optional): The time the PR was in draft state. label_metrics (dict, optional): A dictionary containing the label metrics mentor_activity (dict, optional): A dictionary containing active mentors - + created_at (datetime, optional): The time the issue was created. """ # pylint: disable=too-many-instance-attributes diff --git a/issue_metrics.py b/issue_metrics.py index 7aa2fa46..0084bb4e 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -108,8 +108,6 @@ def get_per_issue_metrics( ) else: num_issues_open += 1 - if env_vars.hide_created_at is False: - issue_with_metrics.created_at = issue.created_at else: if ignore_users and issue.user["login"] in ignore_users: # type: ignore continue @@ -161,10 +159,8 @@ def get_per_issue_metrics( ) elif issue.state == "open": # type: ignore num_issues_open += 1 - if env_vars.hide_created_at is False: - issue_with_metrics.created_at = issue.created_at -if not env_vars.hide_created_at: - issue_with_metrics.created_at = issue["createdAt"] + if not env_vars.hide_created_at: + issue_with_metrics.created_at = issue["createdAt"] issues_with_metrics.append(issue_with_metrics) return issues_with_metrics, num_issues_open, num_issues_closed diff --git a/markdown_writer.py b/markdown_writer.py index 934c01fa..1b515c52 100644 --- a/markdown_writer.py +++ b/markdown_writer.py @@ -81,7 +81,7 @@ def get_non_hidden_columns(labels) -> List[str]: columns.append(f"Time spent in {label}") hide_created_at = env_vars.hide_created_at if not hide_created_at: - columns.append(f'Created At') + columns.append("Created At") return columns From ad3c5e1bb1d7a9096e647a9c73c7ae2d7bb4b330 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Sat, 25 Jan 2025 18:33:22 -0600 Subject: [PATCH 246/358] fix: markdown/prettier lint of README Signed-off-by: jmeridth --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 956c79bf..b08ecea6 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ This action can be configured to authenticate with GitHub App Installation or Pe | `HIDE_TIME_TO_ANSWER` | False | False | If set to `true`, the time to answer a discussion will not be displayed in the generated Markdown file. | | `HIDE_TIME_TO_CLOSE` | False | False | If set to `true`, the time to close will not be displayed in the generated Markdown file. | | `HIDE_TIME_TO_FIRST_RESPONSE` | False | False | If set to `true`, the time to first response will not be displayed in the generated Markdown file. | -| `HIDE_CREATED_AT` | False | True | If set to `true`, the creation timestmap will not be displayed in the generated Markdown file. | +| `HIDE_CREATED_AT` | False | True | If set to `true`, the creation timestmap will not be displayed in the generated Markdown file. | | `DRAFT_PR_TRACKING` | False | False | If set to `true`, draft PRs will be included in the metrics as a new column and in the summary stats. | | `IGNORE_USERS` | False | False | A comma separated list of users to ignore when calculating metrics. (ie. `IGNORE_USERS: 'user1,user2'`). To ignore bots, append `[bot]` to the user (ie. `IGNORE_USERS: 'github-actions[bot]'`) Users in this list will also have their authored issues and pull requests removed from the Markdown table. | | `ENABLE_MENTOR_COUNT` | False | False | If set to 'TRUE' count number of comments users left on discussions, issues and PRs and display number of active mentors | From e07414fece11b87fbc3d0ac3d092d210dde2a853 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Mon, 27 Jan 2025 10:00:24 -0600 Subject: [PATCH 247/358] fix: use correct created_at issue api attribute Signed-off-by: jmeridth --- issue_metrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/issue_metrics.py b/issue_metrics.py index 0084bb4e..20667162 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -160,7 +160,7 @@ def get_per_issue_metrics( elif issue.state == "open": # type: ignore num_issues_open += 1 if not env_vars.hide_created_at: - issue_with_metrics.created_at = issue["createdAt"] + issue_with_metrics.created_at = issue["created_at"] issues_with_metrics.append(issue_with_metrics) return issues_with_metrics, num_issues_open, num_issues_closed From 5f65d036c60caee500124770c2dbfb765e4820ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 17:38:23 +0000 Subject: [PATCH 248/358] chore(deps): bump python from `23a81be` to `026dd41` Bumps python from `23a81be` to `026dd41`. --- updated-dependencies: - dependency-name: python dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index fd95e82a..06422622 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.13-slim@sha256:23a81be7b258c8f516f7a60e80943cace4350deb8204cf107c7993e343610d47 +FROM python:3.13-slim@sha256:026dd417a88d0be8ed5542a05cff5979d17625151be8a1e25a994f85c87962a5 LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From 0fe7226241f3ddfbf58b4c838768b1b22d1d7c82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 17:43:11 +0000 Subject: [PATCH 249/358] chore(deps): bump the dependencies group with 2 updates Bumps the dependencies group with 2 updates: [github/codeql-action](https://github.com/github/codeql-action) and [actions/stale](https://github.com/actions/stale). Updates `github/codeql-action` from 3.28.1 to 3.28.5 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/b6a472f63d85b9c78a3ac5e89422239fc15e9b3c...f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4) Updates `actions/stale` from 9.0.0 to 9.1.0 - [Release notes](https://github.com/actions/stale/releases) - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/stale/compare/v9.0.0...v9.1.0) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: actions/stale dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/scorecard.yml | 2 +- .github/workflows/stale.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 3b5e9024..419ef201 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.24.9 + uses: github/codeql-action/upload-sarif@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.24.9 with: sarif_file: results.sarif diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 2d8c416e..78e8210b 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -11,7 +11,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v9.0.0 + - uses: actions/stale@v9.1.0 with: stale-issue-message: "This issue is stale because it has been open 21 days with no activity. Remove stale label or comment or this will be closed in 14 days." close-issue-message: "This issue was closed because it has been stalled for 35 days with no activity." From 33d8e455a4383529ebac4b01d7db626a8504bbe2 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Mon, 27 Jan 2025 18:23:27 -0600 Subject: [PATCH 250/358] chore: update workflows - [x] update ospo-reusable-workflows version - prevents auto-labeler creating draft releases - [x] update permissions - release-image, allows for attestations, if enabled - auto-labler, content: read Signed-off-by: jmeridth --- .github/release-drafter.yml | 3 ++- .github/workflows/auto-labeler.yml | 2 +- .github/workflows/pr-title.yml | 2 +- .github/workflows/release.yml | 12 ++++++------ 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index fb2ec4ba..f786f739 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -36,11 +36,12 @@ version-resolver: minor: labels: - "enhancement" - - "fix" + - "feature" - "minor" patch: labels: - "documentation" + - "fix" - "maintenance" - "patch" default: patch diff --git a/.github/workflows/auto-labeler.yml b/.github/workflows/auto-labeler.yml index 7e419547..e3860291 100644 --- a/.github/workflows/auto-labeler.yml +++ b/.github/workflows/auto-labeler.yml @@ -11,7 +11,7 @@ jobs: permissions: contents: write pull-requests: write - uses: github/ospo-reusable-workflows/.github/workflows/auto-labeler.yaml@1406afbf7a795f706f04644059cecbb3b2f0c1a0 + uses: github/ospo-reusable-workflows/.github/workflows/auto-labeler.yaml@6a0a6d0de2227f9d5d11af90a87b2e2fd6b5463d with: config-name: release-drafter.yml secrets: diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index 70f19748..c8aee013 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -12,6 +12,6 @@ jobs: contents: read pull-requests: read statuses: write - uses: github/ospo-reusable-workflows/.github/workflows/pr-title.yaml@1406afbf7a795f706f04644059cecbb3b2f0c1a0 + uses: github/ospo-reusable-workflows/.github/workflows/pr-title.yaml@6a0a6d0de2227f9d5d11af90a87b2e2fd6b5463d secrets: github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f551ecef..e0463db3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: permissions: contents: write pull-requests: read - uses: github/ospo-reusable-workflows/.github/workflows/release.yaml@53a9c808122ffaae9af948f72139fb4bd44ab74c + uses: github/ospo-reusable-workflows/.github/workflows/release.yaml@6a0a6d0de2227f9d5d11af90a87b2e2fd6b5463d with: publish: true release-config-name: release-drafter.yml @@ -21,11 +21,11 @@ jobs: release_image: needs: release permissions: - contents: write - discussions: write + contents: read packages: write - pull-requests: read - uses: github/ospo-reusable-workflows/.github/workflows/release-image.yaml@53a9c808122ffaae9af948f72139fb4bd44ab74c + id-token: write + attestations: write + uses: github/ospo-reusable-workflows/.github/workflows/release-image.yaml@6a0a6d0de2227f9d5d11af90a87b2e2fd6b5463d with: image-name: ${{ github.repository_owner }}/issue_metrics full-tag: ${{ needs.release.outputs.full-tag }} @@ -40,7 +40,7 @@ jobs: permissions: contents: read discussions: write - uses: github/ospo-reusable-workflows/.github/workflows/release-discussion.yaml@53a9c808122ffaae9af948f72139fb4bd44ab74c + uses: github/ospo-reusable-workflows/.github/workflows/release-discussion.yaml@6a0a6d0de2227f9d5d11af90a87b2e2fd6b5463d with: full-tag: ${{ needs.release.outputs.full-tag }} body: ${{ needs.release.outputs.body }} From e744c47b35895683409cca2c245394bd57d64a09 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 17:29:44 +0000 Subject: [PATCH 251/358] chore(deps): bump pylint from 3.3.3 to 3.3.4 in the dependencies group Bumps the dependencies group with 1 update: [pylint](https://github.com/pylint-dev/pylint). Updates `pylint` from 3.3.3 to 3.3.4 - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v3.3.3...v3.3.4) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index da947c52..0eb6a5dc 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,7 +2,7 @@ black==24.10.0 flake8==7.1.1 mypy==1.14.1 mypy-extensions==1.0.0 -pylint==3.3.3 +pylint==3.3.4 pytest==8.3.4 pytest-cov==6.0.0 types-pytz==2024.2.0.20241221 From 79b0e33d58336b57a047b395fb92343f537c1d17 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 17:29:53 +0000 Subject: [PATCH 252/358] chore(deps): bump pytz from 2024.2 to 2025.1 Bumps [pytz](https://github.com/stub42/pytz) from 2024.2 to 2025.1. - [Release notes](https://github.com/stub42/pytz/releases) - [Commits](https://github.com/stub42/pytz/compare/release_2024.2...release_2025.1) --- updated-dependencies: - dependency-name: pytz dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8af040dd..66d9b9c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ github3.py==4.0.1 numpy==2.2.2 python-dotenv==1.0.1 -pytz==2024.2 +pytz==2025.1 requests==2.32.3 From 1e6a9541770cff768e19d89093dd8e33587c953b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 17:33:16 +0000 Subject: [PATCH 253/358] chore(deps): bump black from 24.10.0 to 25.1.0 Bumps [black](https://github.com/psf/black) from 24.10.0 to 25.1.0. - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/compare/24.10.0...25.1.0) --- updated-dependencies: - dependency-name: black dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 0eb6a5dc..cf4ed19f 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,4 +1,4 @@ -black==24.10.0 +black==25.1.0 flake8==7.1.1 mypy==1.14.1 mypy-extensions==1.0.0 From 606aa497ddeee411e969f55958d0eb494a738822 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 18:05:06 +0000 Subject: [PATCH 254/358] chore(deps): bump the dependencies group with 3 updates Bumps the dependencies group with 3 updates: [github/contributors](https://github.com/github/contributors), [actions/setup-python](https://github.com/actions/setup-python) and [github/codeql-action](https://github.com/github/codeql-action). Updates `github/contributors` from 1.5.3 to 1.5.4 - [Release notes](https://github.com/github/contributors/releases) - [Commits](https://github.com/github/contributors/compare/695ea9d3f1c31f6ff67ab7d6a964a15f8ef9fa04...3387722e41cbcd1560376fe7575b4b0e372633c5) Updates `actions/setup-python` from 5.3.0 to 5.4.0 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v5.3.0...v5.4.0) Updates `github/codeql-action` from 3.28.5 to 3.28.8 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4...dd746615b3b9d728a6a37ca2045b68ca76d4841a) --- updated-dependencies: - dependency-name: github/contributors dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/contributor_report.yaml | 2 +- .github/workflows/linter.yaml | 2 +- .github/workflows/python-package.yml | 2 +- .github/workflows/scorecard.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/contributor_report.yaml b/.github/workflows/contributor_report.yaml index f3c5041e..f73b89be 100644 --- a/.github/workflows/contributor_report.yaml +++ b/.github/workflows/contributor_report.yaml @@ -27,7 +27,7 @@ jobs: echo "END_DATE=$end_date" >> "$GITHUB_ENV" - name: Run contributor action - uses: github/contributors@695ea9d3f1c31f6ff67ab7d6a964a15f8ef9fa04 # v1.5.3 + uses: github/contributors@3387722e41cbcd1560376fe7575b4b0e372633c5 # v1.5.4 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} START_DATE: ${{ env.START_DATE }} diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index 7572271a..3b2611d3 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -23,7 +23,7 @@ jobs: # Full git history is needed to get a proper # list of changed files within `super-linter` fetch-depth: 0 - - uses: actions/setup-python@v5.3.0 + - uses: actions/setup-python@v5.4.0 with: python-version: "3.12" - name: Install dependencies diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 146b24eb..4a6f0983 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -23,7 +23,7 @@ jobs: steps: - uses: actions/checkout@v4.2.2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5.3.0 + uses: actions/setup-python@v5.4.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 419ef201..3dc9916f 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.24.9 + uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.24.9 with: sarif_file: results.sarif From d05062ef39f4cf60806712e813cf55a00f0f7d9f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 17:09:23 +0000 Subject: [PATCH 255/358] chore(deps): bump python from `026dd41` to `ae9f9ac` Bumps python from `026dd41` to `ae9f9ac`. --- updated-dependencies: - dependency-name: python dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 06422622..21df98b2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.13-slim@sha256:026dd417a88d0be8ed5542a05cff5979d17625151be8a1e25a994f85c87962a5 +FROM python:3.13-slim@sha256:ae9f9ac89467077ed1efefb6d9042132d28134ba201b2820227d46c9effd3174 LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From da5fe38886915948b7e29eeddee0116632189105 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 17:42:25 +0000 Subject: [PATCH 256/358] chore(deps): bump mypy from 1.14.1 to 1.15.0 in the dependencies group Bumps the dependencies group with 1 update: [mypy](https://github.com/python/mypy). Updates `mypy` from 1.14.1 to 1.15.0 - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.14.1...v1.15.0) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index cf4ed19f..052f2fba 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,6 +1,6 @@ black==25.1.0 flake8==7.1.1 -mypy==1.14.1 +mypy==1.15.0 mypy-extensions==1.0.0 pylint==3.3.4 pytest==8.3.4 From d15c9d468f85d99407115772a342eb5b78801514 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 17:43:16 +0000 Subject: [PATCH 257/358] chore(deps): bump github/codeql-action in the dependencies group Bumps the dependencies group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 3.28.8 to 3.28.9 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/dd746615b3b9d728a6a37ca2045b68ca76d4841a...9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 3dc9916f..774e8edc 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.24.9 + uses: github/codeql-action/upload-sarif@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.24.9 with: sarif_file: results.sarif From 28e181a278ec4d9b4a0ace3ac27ecf45684fbfe0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 17:51:16 +0000 Subject: [PATCH 258/358] chore(deps): bump types-pytz from 2024.2.0.20241221 to 2025.1.0.20250204 Bumps [types-pytz](https://github.com/python/typeshed) from 2024.2.0.20241221 to 2025.1.0.20250204. - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-pytz dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 052f2fba..b7a907b4 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -5,5 +5,5 @@ mypy-extensions==1.0.0 pylint==3.3.4 pytest==8.3.4 pytest-cov==6.0.0 -types-pytz==2024.2.0.20241221 +types-pytz==2025.1.0.20250204 types-requests==2.32.0.20241016 From 24979fcef822caa2e9b59323cbc233370bb48707 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 17:39:40 +0000 Subject: [PATCH 259/358] chore(deps): bump github/contributors in the dependencies group Bumps the dependencies group with 1 update: [github/contributors](https://github.com/github/contributors). Updates `github/contributors` from 1.5.4 to 1.5.5 - [Release notes](https://github.com/github/contributors/releases) - [Commits](https://github.com/github/contributors/compare/3387722e41cbcd1560376fe7575b4b0e372633c5...1f903d4eba7d5318fea1ad9500d53370599bf9b8) --- updated-dependencies: - dependency-name: github/contributors dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/contributor_report.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/contributor_report.yaml b/.github/workflows/contributor_report.yaml index f73b89be..231f3cc8 100644 --- a/.github/workflows/contributor_report.yaml +++ b/.github/workflows/contributor_report.yaml @@ -27,7 +27,7 @@ jobs: echo "END_DATE=$end_date" >> "$GITHUB_ENV" - name: Run contributor action - uses: github/contributors@3387722e41cbcd1560376fe7575b4b0e372633c5 # v1.5.4 + uses: github/contributors@1f903d4eba7d5318fea1ad9500d53370599bf9b8 # v1.5.5 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} START_DATE: ${{ env.START_DATE }} From b6f41ce57655bb21dd694f8a2447e833554cb3d3 Mon Sep 17 00:00:00 2001 From: Jake Adams Date: Fri, 28 Feb 2025 08:53:45 -0700 Subject: [PATCH 260/358] Use the `output_file` variable when splitting into smaller files. --- issue_metrics.py | 15 ++++++++------- markdown_writer.py | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index 20667162..11aba8ee 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -350,15 +350,16 @@ def main(): # pragma: no cover output_file=output_file, ) + file_name_without_extension = output_file.split(".")[0] max_char_count = 65535 - if markdown_too_large_for_issue_body("issue_metrics.md", max_char_count): - split_markdown_file("issue_metrics.md", max_char_count) - shutil.move("issue_metrics.md", "issue_metrics_full.md") - shutil.move("issue_metrics_0.md", "issue_metrics.md") + if markdown_too_large_for_issue_body(output_file, max_char_count): + split_markdown_file(output_file, max_char_count) + shutil.move(output_file, f"{file_name_without_extension}_full.md") + shutil.move(f"{file_name_without_extension}_0.md", output_file) print( - "Issue metrics markdown file is too large for GitHub issue body and has been \ -split into multiple files. ie. issue_metrics.md, issue_metrics_1.md, etc. \ -The full file is saved as issue_metrics_full.md\n\ + f"Issue metrics markdown file is too large for GitHub issue body and has been \ +split into multiple files. ie. {output_file}, {file_name_without_extension}_1.md, etc. \ +The full file is saved as {file_name_without_extension}_full.md\n\ See https://github.com/github/issue-metrics/blob/main/docs/dealing-with-large-issue-metrics.md" ) diff --git a/markdown_writer.py b/markdown_writer.py index 1b515c52..e506a4f4 100644 --- a/markdown_writer.py +++ b/markdown_writer.py @@ -225,7 +225,7 @@ def write_to_markdown( if search_query: file.write(f"Search query used to find these items: `{search_query}`\n") - print("Wrote issue metrics to issue_metrics.md") + print(f"Wrote issue metrics to {output_file_name}") def write_overall_metrics_tables( From efe551ac79031acf279d66c775d6137b7f4951ea Mon Sep 17 00:00:00 2001 From: Jake Adams Date: Fri, 28 Feb 2025 09:46:18 -0700 Subject: [PATCH 261/358] Use `pathlib.Path.stem` to extract file name. --- issue_metrics.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/issue_metrics.py b/issue_metrics.py index 11aba8ee..20f29abe 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -16,6 +16,7 @@ """ import shutil +from pathlib import Path from typing import List, Union import github3 @@ -350,7 +351,7 @@ def main(): # pragma: no cover output_file=output_file, ) - file_name_without_extension = output_file.split(".")[0] + file_name_without_extension = Path(output_file).stem max_char_count = 65535 if markdown_too_large_for_issue_body(output_file, max_char_count): split_markdown_file(output_file, max_char_count) From 15cfe4234210db47b4cb7245c9cb0f5f929dc02b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 17:11:21 +0000 Subject: [PATCH 262/358] chore(deps): bump python from `ae9f9ac` to `f3614d9` Bumps python from `ae9f9ac` to `f3614d9`. --- updated-dependencies: - dependency-name: python dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 21df98b2..999d500d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.13-slim@sha256:ae9f9ac89467077ed1efefb6d9042132d28134ba201b2820227d46c9effd3174 +FROM python:3.13-slim@sha256:f3614d98f38b0525d670f287b0474385952e28eb43016655dd003d0e28cf8652 LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From d788ccb7843249c6e31d82d83003defccc35d87c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 17:49:12 +0000 Subject: [PATCH 263/358] chore(deps): bump the dependencies group with 3 updates Bumps the dependencies group with 3 updates: [flake8](https://github.com/pycqa/flake8), [pytest](https://github.com/pytest-dev/pytest) and [numpy](https://github.com/numpy/numpy). Updates `flake8` from 7.1.1 to 7.1.2 - [Commits](https://github.com/pycqa/flake8/compare/7.1.1...7.1.2) Updates `pytest` from 8.3.4 to 8.3.5 - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.3.4...8.3.5) Updates `numpy` from 2.2.2 to 2.2.3 - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.2.2...v2.2.3) --- updated-dependencies: - dependency-name: flake8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 4 ++-- requirements.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index b7a907b4..2b74088b 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,9 +1,9 @@ black==25.1.0 -flake8==7.1.1 +flake8==7.1.2 mypy==1.15.0 mypy-extensions==1.0.0 pylint==3.3.4 -pytest==8.3.4 +pytest==8.3.5 pytest-cov==6.0.0 types-pytz==2025.1.0.20250204 types-requests==2.32.0.20241016 diff --git a/requirements.txt b/requirements.txt index 66d9b9c4..2a750243 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ github3.py==4.0.1 -numpy==2.2.2 +numpy==2.2.3 python-dotenv==1.0.1 pytz==2025.1 requests==2.32.3 From e469832a22ce515331a06150bd771b1333973537 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 17:54:15 +0000 Subject: [PATCH 264/358] chore(deps): bump the dependencies group with 6 updates Bumps the dependencies group with 6 updates: | Package | From | To | | --- | --- | --- | | [github/ospo-reusable-workflows](https://github.com/github/ospo-reusable-workflows) | `0.4.5` | `0.4.6` | | [github/contributors](https://github.com/github/contributors) | `1.5.5` | `1.5.6` | | [super-linter/super-linter](https://github.com/super-linter/super-linter) | `7.2.1` | `7.3.0` | | [ossf/scorecard-action](https://github.com/ossf/scorecard-action) | `2.4.0` | `2.4.1` | | [actions/upload-artifact](https://github.com/actions/upload-artifact) | `4.6.0` | `4.6.1` | | [github/codeql-action](https://github.com/github/codeql-action) | `3.28.9` | `3.28.10` | Updates `github/ospo-reusable-workflows` from 0.4.5 to 0.4.6 - [Release notes](https://github.com/github/ospo-reusable-workflows/releases) - [Changelog](https://github.com/github/ospo-reusable-workflows/blob/main/docs/release-image.md) - [Commits](https://github.com/github/ospo-reusable-workflows/compare/6a0a6d0de2227f9d5d11af90a87b2e2fd6b5463d...ecdd405ebb379e0713e348440e6e26e85fc06773) Updates `github/contributors` from 1.5.5 to 1.5.6 - [Release notes](https://github.com/github/contributors/releases) - [Commits](https://github.com/github/contributors/compare/1f903d4eba7d5318fea1ad9500d53370599bf9b8...ed5a1f1c7c0038f4c9ad790685b6b8a549ab3f64) Updates `super-linter/super-linter` from 7.2.1 to 7.3.0 - [Release notes](https://github.com/super-linter/super-linter/releases) - [Changelog](https://github.com/super-linter/super-linter/blob/main/CHANGELOG.md) - [Commits](https://github.com/super-linter/super-linter/compare/85f7611e0f7b53c8573cca84aa0ed4344f6f6a4d...4e8a7c2bf106c4c766c816b35ec612638dc9b6b2) Updates `ossf/scorecard-action` from 2.4.0 to 2.4.1 - [Release notes](https://github.com/ossf/scorecard-action/releases) - [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md) - [Commits](https://github.com/ossf/scorecard-action/compare/62b2cac7ed8198b15735ed49ab1e5cf35480ba46...f49aabe0b5af0936a0987cfb85d86b75731b0186) Updates `actions/upload-artifact` from 4.6.0 to 4.6.1 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4.6.0...v4.6.1) Updates `github/codeql-action` from 3.28.9 to 3.28.10 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0...b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d) --- updated-dependencies: - dependency-name: github/ospo-reusable-workflows dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: github/contributors dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: super-linter/super-linter dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: ossf/scorecard-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/auto-labeler.yml | 2 +- .github/workflows/contributor_report.yaml | 2 +- .github/workflows/linter.yaml | 2 +- .github/workflows/pr-title.yml | 2 +- .github/workflows/release.yml | 6 +++--- .github/workflows/scorecard.yml | 6 +++--- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/auto-labeler.yml b/.github/workflows/auto-labeler.yml index e3860291..f6da5ce5 100644 --- a/.github/workflows/auto-labeler.yml +++ b/.github/workflows/auto-labeler.yml @@ -11,7 +11,7 @@ jobs: permissions: contents: write pull-requests: write - uses: github/ospo-reusable-workflows/.github/workflows/auto-labeler.yaml@6a0a6d0de2227f9d5d11af90a87b2e2fd6b5463d + uses: github/ospo-reusable-workflows/.github/workflows/auto-labeler.yaml@ecdd405ebb379e0713e348440e6e26e85fc06773 with: config-name: release-drafter.yml secrets: diff --git a/.github/workflows/contributor_report.yaml b/.github/workflows/contributor_report.yaml index 231f3cc8..604a39e7 100644 --- a/.github/workflows/contributor_report.yaml +++ b/.github/workflows/contributor_report.yaml @@ -27,7 +27,7 @@ jobs: echo "END_DATE=$end_date" >> "$GITHUB_ENV" - name: Run contributor action - uses: github/contributors@1f903d4eba7d5318fea1ad9500d53370599bf9b8 # v1.5.5 + uses: github/contributors@ed5a1f1c7c0038f4c9ad790685b6b8a549ab3f64 # v1.5.6 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} START_DATE: ${{ env.START_DATE }} diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index 3b2611d3..6e8db3fc 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -30,7 +30,7 @@ jobs: run: | pip install -r requirements.txt -r requirements-test.txt - name: Lint Code Base - uses: super-linter/super-linter@85f7611e0f7b53c8573cca84aa0ed4344f6f6a4d # v7.2.1 + uses: super-linter/super-linter@4e8a7c2bf106c4c766c816b35ec612638dc9b6b2 # v7.3.0 env: DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index c8aee013..0655a34d 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -12,6 +12,6 @@ jobs: contents: read pull-requests: read statuses: write - uses: github/ospo-reusable-workflows/.github/workflows/pr-title.yaml@6a0a6d0de2227f9d5d11af90a87b2e2fd6b5463d + uses: github/ospo-reusable-workflows/.github/workflows/pr-title.yaml@ecdd405ebb379e0713e348440e6e26e85fc06773 secrets: github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e0463db3..48c53870 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: permissions: contents: write pull-requests: read - uses: github/ospo-reusable-workflows/.github/workflows/release.yaml@6a0a6d0de2227f9d5d11af90a87b2e2fd6b5463d + uses: github/ospo-reusable-workflows/.github/workflows/release.yaml@ecdd405ebb379e0713e348440e6e26e85fc06773 with: publish: true release-config-name: release-drafter.yml @@ -25,7 +25,7 @@ jobs: packages: write id-token: write attestations: write - uses: github/ospo-reusable-workflows/.github/workflows/release-image.yaml@6a0a6d0de2227f9d5d11af90a87b2e2fd6b5463d + uses: github/ospo-reusable-workflows/.github/workflows/release-image.yaml@ecdd405ebb379e0713e348440e6e26e85fc06773 with: image-name: ${{ github.repository_owner }}/issue_metrics full-tag: ${{ needs.release.outputs.full-tag }} @@ -40,7 +40,7 @@ jobs: permissions: contents: read discussions: write - uses: github/ospo-reusable-workflows/.github/workflows/release-discussion.yaml@6a0a6d0de2227f9d5d11af90a87b2e2fd6b5463d + uses: github/ospo-reusable-workflows/.github/workflows/release-discussion.yaml@ecdd405ebb379e0713e348440e6e26e85fc06773 with: full-tag: ${{ needs.release.outputs.full-tag }} body: ${{ needs.release.outputs.body }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 774e8edc..2cf3c836 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -30,18 +30,18 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 + uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 with: results_file: results.sarif results_format: sarif publish_results: true - name: "Upload artifact" - uses: actions/upload-artifact@v4.6.0 + uses: actions/upload-artifact@v4.6.1 with: name: SARIF file path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.24.9 + uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.24.9 with: sarif_file: results.sarif From cba3ffbd07abe9be8f06db095881e454408dca8a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 18:05:36 +0000 Subject: [PATCH 265/358] chore(deps): bump types-requests from 2.32.0.20241016 to 2.32.0.20250301 Bumps [types-requests](https://github.com/python/typeshed) from 2.32.0.20241016 to 2.32.0.20250301. - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 2b74088b..75e75126 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -6,4 +6,4 @@ pylint==3.3.4 pytest==8.3.5 pytest-cov==6.0.0 types-pytz==2025.1.0.20250204 -types-requests==2.32.0.20241016 +types-requests==2.32.0.20250301 From 59e497dbd42d346ca48bf78faffda25eade63d29 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Mon, 3 Mar 2025 12:07:25 -0600 Subject: [PATCH 266/358] fix: linting Signed-off-by: jmeridth --- labels.py | 2 +- markdown_helpers.py | 2 +- search.py | 2 +- test_labels.py | 2 +- test_markdown_helpers.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/labels.py b/labels.py index adbb3339..81b9b4b6 100644 --- a/labels.py +++ b/labels.py @@ -1,4 +1,4 @@ -""" Functions for calculating time spent in labels. """ +"""Functions for calculating time spent in labels.""" from datetime import datetime, timedelta from typing import List diff --git a/markdown_helpers.py b/markdown_helpers.py index 8faf39ac..c06a0692 100644 --- a/markdown_helpers.py +++ b/markdown_helpers.py @@ -1,4 +1,4 @@ -""" Helper functions for working with markdown files. """ +"""Helper functions for working with markdown files.""" def markdown_too_large_for_issue_body(file_path: str, max_char_count: int) -> bool: diff --git a/search.py b/search.py index 5a5c1fa5..742d7e68 100644 --- a/search.py +++ b/search.py @@ -1,4 +1,4 @@ -""" A module to search for issues in a GitHub repository.""" +"""A module to search for issues in a GitHub repository.""" import sys from time import sleep diff --git a/test_labels.py b/test_labels.py index 2fb82121..9c49b2b8 100644 --- a/test_labels.py +++ b/test_labels.py @@ -1,4 +1,4 @@ -""" Unit tests for labels.py """ +"""Unit tests for labels.py""" import unittest from datetime import datetime, timedelta diff --git a/test_markdown_helpers.py b/test_markdown_helpers.py index d856a62e..591843e7 100644 --- a/test_markdown_helpers.py +++ b/test_markdown_helpers.py @@ -1,4 +1,4 @@ -""" Unit tests for the markdown_helpers module. """ +"""Unit tests for the markdown_helpers module.""" import os import unittest From fb050490bea18f8cb0a1b86cd3960f294901aaad Mon Sep 17 00:00:00 2001 From: Jake Adams Date: Wed, 5 Mar 2025 14:44:55 -0700 Subject: [PATCH 267/358] Add `TestEvaluateMarkdownFileSize` and split block to separate function --- issue_metrics.py | 26 ++++++++++--------- test_issue_metrics.py | 60 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 72 insertions(+), 14 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index 20f29abe..c46a4cf7 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -166,6 +166,19 @@ def get_per_issue_metrics( return issues_with_metrics, num_issues_open, num_issues_closed +def evaluate_markdown_file_size(output_file: str) -> None: + file_name_without_extension = Path(output_file).stem + max_char_count = 65535 + if markdown_too_large_for_issue_body(output_file, max_char_count): + split_markdown_file(output_file, max_char_count) + shutil.move(output_file, f"{file_name_without_extension}_full.md") + shutil.move(f"{file_name_without_extension}_0.md", output_file) + print( + f"Issue metrics markdown file is too large for GitHub issue body and has been \ +split into multiple files. ie. {output_file}, {file_name_without_extension}_1.md, etc. \ +The full file is saved as {file_name_without_extension}_full.md\n\ +See https://github.com/github/issue-metrics/blob/main/docs/dealing-with-large-issue-metrics.md" + ) def main(): # pragma: no cover """Run the issue-metrics script. @@ -351,18 +364,7 @@ def main(): # pragma: no cover output_file=output_file, ) - file_name_without_extension = Path(output_file).stem - max_char_count = 65535 - if markdown_too_large_for_issue_body(output_file, max_char_count): - split_markdown_file(output_file, max_char_count) - shutil.move(output_file, f"{file_name_without_extension}_full.md") - shutil.move(f"{file_name_without_extension}_0.md", output_file) - print( - f"Issue metrics markdown file is too large for GitHub issue body and has been \ -split into multiple files. ie. {output_file}, {file_name_without_extension}_1.md, etc. \ -The full file is saved as {file_name_without_extension}_full.md\n\ -See https://github.com/github/issue-metrics/blob/main/docs/dealing-with-large-issue-metrics.md" - ) + evaluate_markdown_file_size(output_file) if __name__ == "__main__": diff --git a/test_issue_metrics.py b/test_issue_metrics.py index 9892cca7..b02dde8a 100644 --- a/test_issue_metrics.py +++ b/test_issue_metrics.py @@ -8,16 +8,17 @@ TestSearchIssues: A class to test the search_issues function. TestGetPerIssueMetrics: A class to test the get_per_issue_metrics function. TestGetEnvVars: A class to test the get_env_vars function. - + TestEvaluateMarkdownFileSize: A class to test the evaluate_markdown_file_size function. """ import os import unittest from datetime import datetime, timedelta -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, call, patch from issue_metrics import ( IssueWithMetrics, + evaluate_markdown_file_size, get_env_vars, get_per_issue_metrics, measure_time_to_close, @@ -176,6 +177,7 @@ def test_get_per_issue_metrics_with_hide_envs(self): "HIDE_TIME_TO_ANSWER": "false", "HIDE_TIME_TO_CLOSE": "false", "HIDE_TIME_TO_FIRST_RESPONSE": "false", + "OUTPUT_FILE": "test_issue_metrics.md" }, ) def test_get_per_issue_metrics_without_hide_envs(self): @@ -478,5 +480,59 @@ def test_get_per_issue_metrics_with_discussion_with_hide_envs(self): self.assertEqual(metrics[0][1].time_to_first_response, None) +class TestEvaluateMarkdownFileSize(unittest.TestCase): + """Test suite for the evaluate_markdown_file_size function.""" + + @patch.dict( + os.environ, + { + "GH_TOKEN": "test_token", + "SEARCH_QUERY": "is:issue is:open repo:user/repo", + "OUTPUT_FILE": "test_issue_metrics.md" + } + ) + @patch("issue_metrics.markdown_too_large_for_issue_body") + def test_markdown_too_large_for_issue_body_called_with_output_file(self, mock_evaluate): + """ + Test that the function uses the output_file. + """ + mock_evaluate.return_value = False + evaluate_markdown_file_size("test_issue_metrics.md") + + mock_evaluate.assert_called_with("test_issue_metrics.md", 65535) + + @patch.dict( + os.environ, + { + "GH_TOKEN": "test_token", + "SEARCH_QUERY": "is:issue is:open repo:user/repo", + "OUTPUT_FILE": "test_issue_metrics.md" + } + ) + @patch("issue_metrics.print") + @patch("shutil.move") + @patch("issue_metrics.split_markdown_file") + @patch("issue_metrics.markdown_too_large_for_issue_body") + def test_split_markdown_file_when_file_size_too_large(self, mock_evaluate, mock_split, mock_move, mock_print): + """ + Test that the function is called with the output_file + environment variable. + """ + mock_evaluate.return_value = True + evaluate_markdown_file_size("test_issue_metrics.md") + + mock_split.assert_called_with("test_issue_metrics.md", 65535) + mock_move.assert_has_calls([ + call("test_issue_metrics.md", "test_issue_metrics_full.md"), + call("test_issue_metrics_0.md", "test_issue_metrics.md") + ]) + mock_print.assert_called_with( + "Issue metrics markdown file is too large for GitHub issue body and has been \ +split into multiple files. ie. test_issue_metrics.md, test_issue_metrics_1.md, etc. \ +The full file is saved as test_issue_metrics_full.md\n\ +See https://github.com/github/issue-metrics/blob/main/docs/dealing-with-large-issue-metrics.md" + ) + + if __name__ == "__main__": unittest.main() From 7047248b6e282b80e27f111d06f81bacfb247bc6 Mon Sep 17 00:00:00 2001 From: Jake Adams Date: Wed, 5 Mar 2025 15:04:08 -0700 Subject: [PATCH 268/358] Fix lint errors and remove obsolete code. --- issue_metrics.py | 2 ++ test_issue_metrics.py | 37 +++++++++++++------------------------ 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index c46a4cf7..1a3d63d1 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -166,6 +166,7 @@ def get_per_issue_metrics( return issues_with_metrics, num_issues_open, num_issues_closed + def evaluate_markdown_file_size(output_file: str) -> None: file_name_without_extension = Path(output_file).stem max_char_count = 65535 @@ -180,6 +181,7 @@ def evaluate_markdown_file_size(output_file: str) -> None: See https://github.com/github/issue-metrics/blob/main/docs/dealing-with-large-issue-metrics.md" ) + def main(): # pragma: no cover """Run the issue-metrics script. diff --git a/test_issue_metrics.py b/test_issue_metrics.py index b02dde8a..4f67d393 100644 --- a/test_issue_metrics.py +++ b/test_issue_metrics.py @@ -177,7 +177,6 @@ def test_get_per_issue_metrics_with_hide_envs(self): "HIDE_TIME_TO_ANSWER": "false", "HIDE_TIME_TO_CLOSE": "false", "HIDE_TIME_TO_FIRST_RESPONSE": "false", - "OUTPUT_FILE": "test_issue_metrics.md" }, ) def test_get_per_issue_metrics_without_hide_envs(self): @@ -483,16 +482,10 @@ def test_get_per_issue_metrics_with_discussion_with_hide_envs(self): class TestEvaluateMarkdownFileSize(unittest.TestCase): """Test suite for the evaluate_markdown_file_size function.""" - @patch.dict( - os.environ, - { - "GH_TOKEN": "test_token", - "SEARCH_QUERY": "is:issue is:open repo:user/repo", - "OUTPUT_FILE": "test_issue_metrics.md" - } - ) @patch("issue_metrics.markdown_too_large_for_issue_body") - def test_markdown_too_large_for_issue_body_called_with_output_file(self, mock_evaluate): + def test_markdown_too_large_for_issue_body_called_with_output_file( + self, mock_evaluate + ): """ Test that the function uses the output_file. """ @@ -501,19 +494,13 @@ def test_markdown_too_large_for_issue_body_called_with_output_file(self, mock_ev mock_evaluate.assert_called_with("test_issue_metrics.md", 65535) - @patch.dict( - os.environ, - { - "GH_TOKEN": "test_token", - "SEARCH_QUERY": "is:issue is:open repo:user/repo", - "OUTPUT_FILE": "test_issue_metrics.md" - } - ) @patch("issue_metrics.print") @patch("shutil.move") @patch("issue_metrics.split_markdown_file") @patch("issue_metrics.markdown_too_large_for_issue_body") - def test_split_markdown_file_when_file_size_too_large(self, mock_evaluate, mock_split, mock_move, mock_print): + def test_split_markdown_file_when_file_size_too_large( + self, mock_evaluate, mock_split, mock_move, mock_print + ): """ Test that the function is called with the output_file environment variable. @@ -522,16 +509,18 @@ def test_split_markdown_file_when_file_size_too_large(self, mock_evaluate, mock_ evaluate_markdown_file_size("test_issue_metrics.md") mock_split.assert_called_with("test_issue_metrics.md", 65535) - mock_move.assert_has_calls([ - call("test_issue_metrics.md", "test_issue_metrics_full.md"), - call("test_issue_metrics_0.md", "test_issue_metrics.md") - ]) + mock_move.assert_has_calls( + [ + call("test_issue_metrics.md", "test_issue_metrics_full.md"), + call("test_issue_metrics_0.md", "test_issue_metrics.md"), + ] + ) mock_print.assert_called_with( "Issue metrics markdown file is too large for GitHub issue body and has been \ split into multiple files. ie. test_issue_metrics.md, test_issue_metrics_1.md, etc. \ The full file is saved as test_issue_metrics_full.md\n\ See https://github.com/github/issue-metrics/blob/main/docs/dealing-with-large-issue-metrics.md" - ) + ) if __name__ == "__main__": From 4f1f388c3730f03aeaf306968223420221f59424 Mon Sep 17 00:00:00 2001 From: Jake Adams Date: Wed, 5 Mar 2025 15:08:58 -0700 Subject: [PATCH 269/358] Add missing doc-string to new function --- issue_metrics.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/issue_metrics.py b/issue_metrics.py index 1a3d63d1..055618a5 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -168,6 +168,9 @@ def get_per_issue_metrics( def evaluate_markdown_file_size(output_file: str) -> None: + """ + Evaluate the size of the markdown file and split it, if it is too large. + """ file_name_without_extension = Path(output_file).stem max_char_count = 65535 if markdown_too_large_for_issue_body(output_file, max_char_count): From 6d61329294fd1c9320350e8602f19abb68295a7a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Mar 2025 17:49:26 +0000 Subject: [PATCH 270/358] chore(deps): bump github/codeql-action in the dependencies group Bumps the dependencies group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 3.28.10 to 3.28.11 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d...6bb031afdd8eb862ea3fc1848194185e076637e5) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 2cf3c836..e2dd8b5f 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.24.9 + uses: github/codeql-action/upload-sarif@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.24.9 with: sarif_file: results.sarif From 8799a9ff578236c50270012e33a909df3e7af685 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Mar 2025 18:33:28 +0000 Subject: [PATCH 271/358] chore(deps): bump types-requests from 2.32.0.20250301 to 2.32.0.20250306 Bumps [types-requests](https://github.com/python/typeshed) from 2.32.0.20250301 to 2.32.0.20250306. - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 75e75126..985fbd9f 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -6,4 +6,4 @@ pylint==3.3.4 pytest==8.3.5 pytest-cov==6.0.0 types-pytz==2025.1.0.20250204 -types-requests==2.32.0.20250301 +types-requests==2.32.0.20250306 From 828acb609d5e7541555f028ba10521a93755f085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Schmitz=20von=20H=C3=BClst?= Date: Fri, 14 Mar 2025 09:56:15 +0100 Subject: [PATCH 272/358] Set default filename, when output file is empty --- issue_metrics.py | 13 +++++++------ test_issue_metrics.py | 12 ++++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index 055618a5..4dcd7b62 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -171,15 +171,16 @@ def evaluate_markdown_file_size(output_file: str) -> None: """ Evaluate the size of the markdown file and split it, if it is too large. """ - file_name_without_extension = Path(output_file).stem + output_file_name = output_file if output_file else "issue_metrics.md" + file_name_without_extension = Path(output_file_name).stem max_char_count = 65535 - if markdown_too_large_for_issue_body(output_file, max_char_count): - split_markdown_file(output_file, max_char_count) - shutil.move(output_file, f"{file_name_without_extension}_full.md") - shutil.move(f"{file_name_without_extension}_0.md", output_file) + if markdown_too_large_for_issue_body(output_file_name, max_char_count): + split_markdown_file(output_file_name, max_char_count) + shutil.move(output_file_name, f"{file_name_without_extension}_full.md") + shutil.move(f"{file_name_without_extension}_0.md", output_file_name) print( f"Issue metrics markdown file is too large for GitHub issue body and has been \ -split into multiple files. ie. {output_file}, {file_name_without_extension}_1.md, etc. \ +split into multiple files. ie. {output_file_name}, {file_name_without_extension}_1.md, etc. \ The full file is saved as {file_name_without_extension}_full.md\n\ See https://github.com/github/issue-metrics/blob/main/docs/dealing-with-large-issue-metrics.md" ) diff --git a/test_issue_metrics.py b/test_issue_metrics.py index 4f67d393..97cc22e3 100644 --- a/test_issue_metrics.py +++ b/test_issue_metrics.py @@ -482,6 +482,18 @@ def test_get_per_issue_metrics_with_discussion_with_hide_envs(self): class TestEvaluateMarkdownFileSize(unittest.TestCase): """Test suite for the evaluate_markdown_file_size function.""" + @patch("issue_metrics.markdown_too_large_for_issue_body") + def test_markdown_too_large_for_issue_body_called_with_empty_output_file( + self, mock_evaluate + ): + """ + Test that the function uses the output_file. + """ + mock_evaluate.return_value = False + evaluate_markdown_file_size("") + + mock_evaluate.assert_called_with("issue_metrics.md", 65535) + @patch("issue_metrics.markdown_too_large_for_issue_body") def test_markdown_too_large_for_issue_body_called_with_output_file( self, mock_evaluate From e9e016ae387cbf42e7857738545b427b67359c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Schmitz=20von=20H=C3=BClst?= Date: Fri, 14 Mar 2025 10:00:55 +0100 Subject: [PATCH 273/358] Formatting --- test_issue_metrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_issue_metrics.py b/test_issue_metrics.py index 97cc22e3..728e6711 100644 --- a/test_issue_metrics.py +++ b/test_issue_metrics.py @@ -484,7 +484,7 @@ class TestEvaluateMarkdownFileSize(unittest.TestCase): @patch("issue_metrics.markdown_too_large_for_issue_body") def test_markdown_too_large_for_issue_body_called_with_empty_output_file( - self, mock_evaluate + self, mock_evaluate ): """ Test that the function uses the output_file. From be847c865907d8103e3e300aec0c3be0995acf97 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Fri, 14 Mar 2025 07:49:18 -0500 Subject: [PATCH 274/358] fix: handle created_at with diff types Fixes #489 We union dict and github3.search.IssueSearchResult types but their created at attributes are accessed differently. issue["createdAt"] for dict issue.issue.created_at for github3.search.IssueSearchResult - [x] turned off HIDE_CREATED_AT on some tests to ensure we are handling that scenario Signed-off-by: jmeridth --- issue_metrics.py | 5 ++++- test_config.py | 1 + test_issue_metrics.py | 9 +++------ test_markdown_writer.py | 33 +++++++++++++++++++++------------ 4 files changed, 29 insertions(+), 19 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index 4dcd7b62..ff3c6ca4 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -161,7 +161,10 @@ def get_per_issue_metrics( elif issue.state == "open": # type: ignore num_issues_open += 1 if not env_vars.hide_created_at: - issue_with_metrics.created_at = issue["created_at"] + if isinstance(issue, github3.search.IssueSearchResult): # type: ignore + issue_with_metrics.created_at = issue.issue.created_at # type: ignore + elif isinstance(issue, dict): # type: ignore + issue_with_metrics.created_at = issue["createdAt"] # type: ignore issues_with_metrics.append(issue_with_metrics) return issues_with_metrics, num_issues_open, num_issues_closed diff --git a/test_config.py b/test_config.py index 7a41ad10..327f851a 100644 --- a/test_config.py +++ b/test_config.py @@ -72,6 +72,7 @@ def setUp(self): "GH_TOKEN", "GHE", "HIDE_AUTHOR", + "HIDE_CREATED_AT", "HIDE_ITEMS_CLOSED_COUNT", "HIDE_LABEL_METRICS", "HIDE_TIME_TO_ANSWER", diff --git a/test_issue_metrics.py b/test_issue_metrics.py index 728e6711..5a6e9de6 100644 --- a/test_issue_metrics.py +++ b/test_issue_metrics.py @@ -374,9 +374,7 @@ def setUp(self): self.issue1 = { "title": "Issue 1", "url": "github.com/user/repo/issues/1", - "user": { - "login": "alice", - }, + "user": {"login": "alice"}, "createdAt": "2023-01-01T00:00:00Z", "comments": { "nodes": [ @@ -392,9 +390,7 @@ def setUp(self): self.issue2 = { "title": "Issue 2", "url": "github.com/user/repo/issues/2", - "user": { - "login": "bob", - }, + "user": {"login": "bob"}, "createdAt": "2023-01-01T00:00:00Z", "comments": {"nodes": [{"createdAt": "2023-01-03T00:00:00Z"}]}, "answerChosenAt": "2023-01-05T00:00:00Z", @@ -441,6 +437,7 @@ def test_get_per_issue_metrics_with_discussion(self): "GH_TOKEN": "test_token", "SEARCH_QUERY": "is:issue is:open repo:user/repo", "HIDE_AUTHOR": "true", + "HIDE_CREATED_AT": "false", "HIDE_LABEL_METRICS": "true", "HIDE_TIME_TO_ANSWER": "true", "HIDE_TIME_TO_CLOSE": "true", diff --git a/test_markdown_writer.py b/test_markdown_writer.py index 25aec529..1d63db51 100644 --- a/test_markdown_writer.py +++ b/test_markdown_writer.py @@ -22,6 +22,7 @@ "SEARCH_QUERY": "is:open repo:user/repo", "GH_TOKEN": "test_token", "DRAFT_PR_TRACKING": "True", + "HIDE_CREATED_AT": "False", }, ) class TestWriteToMarkdown(unittest.TestCase): @@ -44,6 +45,7 @@ def test_write_to_markdown(self): title="Issue 1", html_url="https://github.com/user/repo/issues/1", author="alice", + created_at=timedelta(days=-5), time_to_first_response=timedelta(days=1), time_to_close=timedelta(days=2), time_to_answer=timedelta(days=3), @@ -54,6 +56,7 @@ def test_write_to_markdown(self): title="Issue 2\r", html_url="https://github.com/user/repo/issues/2", author="bob", + created_at=timedelta(days=-5), time_to_first_response=timedelta(days=3), time_to_close=timedelta(days=4), time_to_answer=timedelta(days=5), @@ -129,12 +132,12 @@ def test_write_to_markdown(self): "| Number of most active mentors | 5 |\n" "| Total number of items created | 2 |\n\n" "| Title | URL | Author | Time to first response | Time to close |" - " Time to answer | Time in draft | Time spent in bug |\n" - "| --- | --- | --- | --- | --- | --- | --- | --- |\n" + " Time to answer | Time in draft | Time spent in bug | Created At |\n" + "| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n" "| Issue 1 | https://github.com/user/repo/issues/1 | [alice](https://github.com/alice) | 1 day, 0:00:00 | " - "2 days, 0:00:00 | 3 days, 0:00:00 | 1 day, 0:00:00 | 4 days, 0:00:00 |\n" + "2 days, 0:00:00 | 3 days, 0:00:00 | 1 day, 0:00:00 | 4 days, 0:00:00 | -5 days, 0:00:00 |\n" "| Issue 2 | https://github.com/user/repo/issues/2 | [bob](https://github.com/bob) | 3 days, 0:00:00 | " - "4 days, 0:00:00 | 5 days, 0:00:00 | 1 day, 0:00:00 | 2 days, 0:00:00 |\n\n" + "4 days, 0:00:00 | 5 days, 0:00:00 | 1 day, 0:00:00 | 2 days, 0:00:00 | -5 days, 0:00:00 |\n\n" "_This report was generated with the [Issue Metrics Action](https://github.com/github/issue-metrics)_\n" "Search query used to find these items: `is:issue is:open label:bug`\n" ) @@ -156,6 +159,7 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): title="Issue 1", html_url="https://github.com/user/repo/issues/1", author="alice", + created_at=timedelta(days=-5), time_to_first_response=timedelta(days=1), time_to_close=timedelta(days=2), time_to_answer=timedelta(days=3), @@ -166,6 +170,7 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): title="feat| Issue 2", # title contains a vertical bar html_url="https://github.com/user/repo/issues/2", author="bob", + created_at=timedelta(days=-5), time_to_first_response=timedelta(days=3), time_to_close=timedelta(days=4), time_to_answer=timedelta(days=5), @@ -238,12 +243,12 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): "| Number of most active mentors | 5 |\n" "| Total number of items created | 2 |\n\n" "| Title | URL | Author | Time to first response | Time to close |" - " Time to answer | Time in draft | Time spent in bug |\n" - "| --- | --- | --- | --- | --- | --- | --- | --- |\n" + " Time to answer | Time in draft | Time spent in bug | Created At |\n" + "| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n" "| Issue 1 | https://github.com/user/repo/issues/1 | [alice](https://github.com/alice) | 1 day, 0:00:00 | " - "2 days, 0:00:00 | 3 days, 0:00:00 | 1 day, 0:00:00 | 1 day, 0:00:00 |\n" + "2 days, 0:00:00 | 3 days, 0:00:00 | 1 day, 0:00:00 | 1 day, 0:00:00 | -5 days, 0:00:00 |\n" "| feat| Issue 2 | https://github.com/user/repo/issues/2 | [bob](https://github.com/bob) | 3 days, 0:00:00 | " - "4 days, 0:00:00 | 5 days, 0:00:00 | None | 2 days, 0:00:00 |\n\n" + "4 days, 0:00:00 | 5 days, 0:00:00 | None | 2 days, 0:00:00 | -5 days, 0:00:00 |\n\n" "_This report was generated with the [Issue Metrics Action](https://github.com/github/issue-metrics)_\n" ) self.assertEqual(content, expected_content) @@ -287,6 +292,7 @@ def test_write_to_markdown_no_issues(self): { "SEARCH_QUERY": "is:open repo:user/repo", "GH_TOKEN": "test_token", + "HIDE_CREATED_AT": "False", "HIDE_TIME_TO_FIRST_RESPONSE": "True", "HIDE_TIME_TO_CLOSE": "True", "HIDE_TIME_TO_ANSWER": "True", @@ -309,6 +315,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): title="Issue 1", html_url="https://github.com/user/repo/issues/1", author="alice", + created_at=timedelta(days=-5), time_to_first_response=timedelta(minutes=10), time_to_close=timedelta(days=1), time_to_answer=timedelta(hours=2), @@ -321,6 +328,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): title="Issue 2", html_url="https://github.com/user/repo/issues/2", author="bob", + created_at=timedelta(days=-5), time_to_first_response=timedelta(minutes=20), time_to_close=timedelta(days=2), time_to_answer=timedelta(hours=4), @@ -363,6 +371,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): # Check that the function writes the correct markdown file with open("issue_metrics.md", "r", encoding="utf-8") as file: content = file.read() + expected_content = ( "# Issue Metrics\n\n" "| Metric | Count |\n" @@ -370,10 +379,10 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): "| Number of items that remain open | 2 |\n" "| Number of most active mentors | 5 |\n" "| Total number of items created | 2 |\n\n" - "| Title | URL | Author |\n" - "| --- | --- | --- |\n" - "| Issue 1 | https://www.github.com/user/repo/issues/1 | [alice](https://github.com/alice) |\n" - "| Issue 2 | https://www.github.com/user/repo/issues/2 | [bob](https://github.com/bob) |\n\n" + "| Title | URL | Author | Created At |\n" + "| --- | --- | --- | --- |\n" + "| Issue 1 | https://www.github.com/user/repo/issues/1 | [alice](https://github.com/alice) | -5 days, 0:00:00 |\n" + "| Issue 2 | https://www.github.com/user/repo/issues/2 | [bob](https://github.com/bob) | -5 days, 0:00:00 |\n\n" "_This report was generated with the [Issue Metrics Action](https://github.com/github/issue-metrics)_\n" "Search query used to find these items: `repo:user/repo is:issue`\n" ) From fa5f0ca040585e453eea0166f8a8b7fb3f83f7c1 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Sat, 15 Mar 2025 22:19:54 -0500 Subject: [PATCH 275/358] fix: ensure created_at shows up in json - [x] update git version installed in image Signed-off-by: jmeridth --- Dockerfile | 2 +- json_writer.py | 1 + test_json_writer.py | 8 ++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 999d500d..1f4595e5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ COPY requirements.txt *.py /action/workspace/ RUN python3 -m pip install --no-cache-dir -r requirements.txt \ && apt-get -y update \ - && apt-get -y install --no-install-recommends git=1:2.39.5-0+deb12u1 \ + && apt-get -y install --no-install-recommends git=1:2.39.5-0+deb12u2 \ && rm -rf /var/lib/apt/lists/* CMD ["/action/workspace/issue_metrics.py"] diff --git a/json_writer.py b/json_writer.py index 015dbe44..6e388968 100644 --- a/json_writer.py +++ b/json_writer.py @@ -182,6 +182,7 @@ def write_to_json( "time_to_answer": str(issue.time_to_answer), "time_in_draft": str(issue.time_in_draft), "label_metrics": formatted_label_metrics, + "created_at": str(issue.created_at), } ) diff --git a/test_json_writer.py b/test_json_writer.py index 1525d6c6..02278dfe 100644 --- a/test_json_writer.py +++ b/test_json_writer.py @@ -28,6 +28,7 @@ def test_write_to_json(self): labels_metrics={ "bug": timedelta(days=1, hours=16, minutes=24, seconds=12) }, + created_at=timedelta(days=-5), ), IssueWithMetrics( title="Issue 2", @@ -37,6 +38,7 @@ def test_write_to_json(self): time_to_close=timedelta(days=4), time_to_answer=timedelta(days=1), labels_metrics={}, + created_at=timedelta(days=-5), ), ] @@ -99,6 +101,7 @@ def test_write_to_json(self): "time_to_answer": "None", "time_in_draft": "1 day, 0:00:00", "label_metrics": {"bug": "1 day, 16:24:12"}, + "created_at": "-5 days, 0:00:00", }, { "title": "Issue 2", @@ -109,6 +112,7 @@ def test_write_to_json(self): "time_to_answer": "1 day, 0:00:00", "time_in_draft": "None", "label_metrics": {}, + "created_at": "-5 days, 0:00:00", }, ], "search_query": "is:issue repo:owner/repo", @@ -143,6 +147,7 @@ def test_write_to_json_with_no_response(self): time_to_close=None, time_to_answer=None, labels_metrics={}, + created_at=None, ), IssueWithMetrics( title="Issue 2", @@ -152,6 +157,7 @@ def test_write_to_json_with_no_response(self): time_to_close=None, time_to_answer=None, labels_metrics={}, + created_at=None, ), ] @@ -198,6 +204,7 @@ def test_write_to_json_with_no_response(self): "time_to_answer": "None", "time_in_draft": "None", "label_metrics": {}, + "created_at": "None", }, { "title": "Issue 2", @@ -208,6 +215,7 @@ def test_write_to_json_with_no_response(self): "time_to_answer": "None", "time_in_draft": "None", "label_metrics": {}, + "created_at": "None", }, ], "search_query": "is:issue repo:owner/repo", From 050326bbee74540ca9ad006b5ad15b451cac7474 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 17:48:47 +0000 Subject: [PATCH 276/358] chore(deps): bump numpy from 2.2.3 to 2.2.4 in the dependencies group Bumps the dependencies group with 1 update: [numpy](https://github.com/numpy/numpy). Updates `numpy` from 2.2.3 to 2.2.4 - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.2.3...v2.2.4) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2a750243..a0cb9485 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ github3.py==4.0.1 -numpy==2.2.3 +numpy==2.2.4 python-dotenv==1.0.1 pytz==2025.1 requests==2.32.3 From 33c023da9faed217f8e934775fd56ae055982ca7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Mar 2025 17:36:42 +0000 Subject: [PATCH 277/358] chore(deps): bump the dependencies group with 2 updates Bumps the dependencies group with 2 updates: [actions/upload-artifact](https://github.com/actions/upload-artifact) and [github/codeql-action](https://github.com/github/codeql-action). Updates `actions/upload-artifact` from 4.6.1 to 4.6.2 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4.6.1...v4.6.2) Updates `github/codeql-action` from 3.28.11 to 3.28.13 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/6bb031afdd8eb862ea3fc1848194185e076637e5...1b549b9259bda1cb5ddde3b41741a82a2d15a841) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/scorecard.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index e2dd8b5f..ef7ee63a 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -36,12 +36,12 @@ jobs: results_format: sarif publish_results: true - name: "Upload artifact" - uses: actions/upload-artifact@v4.6.1 + uses: actions/upload-artifact@v4.6.2 with: name: SARIF file path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.24.9 + uses: github/codeql-action/upload-sarif@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.24.9 with: sarif_file: results.sarif From 4894f994741e058a87ea8c7175975337882db05e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Mar 2025 18:21:44 +0000 Subject: [PATCH 278/358] chore(deps): bump python from `f3614d9` to `8f3aba4` Bumps python from `f3614d9` to `8f3aba4`. --- updated-dependencies: - dependency-name: python dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 1f4595e5..e2d475cf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.13-slim@sha256:f3614d98f38b0525d670f287b0474385952e28eb43016655dd003d0e28cf8652 +FROM python:3.13-slim@sha256:8f3aba466a471c0ab903dbd7cb979abd4bda370b04789d25440cc90372b50e04 LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From 10c915ad0f52b38bc216ab3fb4b83104a2b5ae13 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Mar 2025 18:35:29 +0000 Subject: [PATCH 279/358] chore(deps): bump pylint from 3.3.4 to 3.3.6 in the dependencies group Bumps the dependencies group with 1 update: [pylint](https://github.com/pylint-dev/pylint). Updates `pylint` from 3.3.4 to 3.3.6 - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v3.3.4...v3.3.6) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 985fbd9f..c624856c 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,7 +2,7 @@ black==25.1.0 flake8==7.1.2 mypy==1.15.0 mypy-extensions==1.0.0 -pylint==3.3.4 +pylint==3.3.6 pytest==8.3.5 pytest-cov==6.0.0 types-pytz==2025.1.0.20250204 From d71bb6644b4f6b48bf64a6494ee2c7c4b7760f8e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Mar 2025 18:41:49 +0000 Subject: [PATCH 280/358] chore(deps): bump types-pytz from 2025.1.0.20250204 to 2025.1.0.20250318 Bumps [types-pytz](https://github.com/python/typeshed) from 2025.1.0.20250204 to 2025.1.0.20250318. - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-pytz dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index c624856c..9d53069b 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -5,5 +5,5 @@ mypy-extensions==1.0.0 pylint==3.3.6 pytest==8.3.5 pytest-cov==6.0.0 -types-pytz==2025.1.0.20250204 +types-pytz==2025.1.0.20250318 types-requests==2.32.0.20250306 From 46537ff620a82cb6d08fa50d5ed5df34fc56b20c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 18:01:45 +0000 Subject: [PATCH 281/358] chore(deps): bump actions/setup-python in the dependencies group Bumps the dependencies group with 1 update: [actions/setup-python](https://github.com/actions/setup-python). Updates `actions/setup-python` from 5.4.0 to 5.5.0 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v5.4.0...v5.5.0) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/linter.yaml | 2 +- .github/workflows/python-package.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index 6e8db3fc..de76ffc9 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -23,7 +23,7 @@ jobs: # Full git history is needed to get a proper # list of changed files within `super-linter` fetch-depth: 0 - - uses: actions/setup-python@v5.4.0 + - uses: actions/setup-python@v5.5.0 with: python-version: "3.12" - name: Install dependencies diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 4a6f0983..5258d184 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -23,7 +23,7 @@ jobs: steps: - uses: actions/checkout@v4.2.2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5.4.0 + uses: actions/setup-python@v5.5.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From f2fd76034460c36b2bec52b129c4e9c93d03b02c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 18:02:05 +0000 Subject: [PATCH 282/358] chore(deps): bump the dependencies group with 4 updates Bumps the dependencies group with 4 updates: [flake8](https://github.com/pycqa/flake8), [types-pytz](https://github.com/python/typeshed), [python-dotenv](https://github.com/theskumar/python-dotenv) and [pytz](https://github.com/stub42/pytz). Updates `flake8` from 7.1.2 to 7.2.0 - [Commits](https://github.com/pycqa/flake8/compare/7.1.2...7.2.0) Updates `types-pytz` from 2025.1.0.20250318 to 2025.2.0.20250326 - [Commits](https://github.com/python/typeshed/commits) Updates `python-dotenv` from 1.0.1 to 1.1.0 - [Release notes](https://github.com/theskumar/python-dotenv/releases) - [Changelog](https://github.com/theskumar/python-dotenv/blob/main/CHANGELOG.md) - [Commits](https://github.com/theskumar/python-dotenv/compare/v1.0.1...v1.1.0) Updates `pytz` from 2025.1 to 2025.2 - [Release notes](https://github.com/stub42/pytz/releases) - [Commits](https://github.com/stub42/pytz/compare/release_2025.1...release_2025.2) --- updated-dependencies: - dependency-name: flake8 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: types-pytz dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: python-dotenv dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: pytz dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 4 ++-- requirements.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index 9d53069b..e3c5501c 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,9 +1,9 @@ black==25.1.0 -flake8==7.1.2 +flake8==7.2.0 mypy==1.15.0 mypy-extensions==1.0.0 pylint==3.3.6 pytest==8.3.5 pytest-cov==6.0.0 -types-pytz==2025.1.0.20250318 +types-pytz==2025.2.0.20250326 types-requests==2.32.0.20250306 diff --git a/requirements.txt b/requirements.txt index a0cb9485..a2f09048 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ github3.py==4.0.1 numpy==2.2.4 -python-dotenv==1.0.1 -pytz==2025.1 +python-dotenv==1.1.0 +pytz==2025.2 requests==2.32.3 From 84cb8d4790f8cf4112f11610abddae5f7728bfd4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 18:05:51 +0000 Subject: [PATCH 283/358] chore(deps): bump types-requests from 2.32.0.20250306 to 2.32.0.20250328 Bumps [types-requests](https://github.com/python/typeshed) from 2.32.0.20250306 to 2.32.0.20250328. - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index e3c5501c..f4e0afd0 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -6,4 +6,4 @@ pylint==3.3.6 pytest==8.3.5 pytest-cov==6.0.0 types-pytz==2025.2.0.20250326 -types-requests==2.32.0.20250306 +types-requests==2.32.0.20250328 From bfd15465fa170544efef915969adbc9ecdefb2d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 17:52:46 +0000 Subject: [PATCH 284/358] chore(deps): bump github/codeql-action in the dependencies group Bumps the dependencies group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 3.28.13 to 3.28.14 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/1b549b9259bda1cb5ddde3b41741a82a2d15a841...fc7e4a0fa01c3cca5fd6a1fddec5c0740c977aa2) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.28.14 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index ef7ee63a..b74fbe18 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.24.9 + uses: github/codeql-action/upload-sarif@fc7e4a0fa01c3cca5fd6a1fddec5c0740c977aa2 # v3.24.9 with: sarif_file: results.sarif From c8e591870e0e1bccb85f81faf9d16f4f16e7b77a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 18:18:27 +0000 Subject: [PATCH 285/358] chore(deps): bump pytest-cov in the dependencies group Bumps the dependencies group with 1 update: [pytest-cov](https://github.com/pytest-dev/pytest-cov). Updates `pytest-cov` from 6.0.0 to 6.1.1 - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v6.0.0...v6.1.1) --- updated-dependencies: - dependency-name: pytest-cov dependency-version: 6.1.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index f4e0afd0..7d6d42c6 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -4,6 +4,6 @@ mypy==1.15.0 mypy-extensions==1.0.0 pylint==3.3.6 pytest==8.3.5 -pytest-cov==6.0.0 +pytest-cov==6.1.1 types-pytz==2025.2.0.20250326 types-requests==2.32.0.20250328 From 4718c931f21c0ca45fc8a57002599c952e0ca77b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 17:20:33 +0000 Subject: [PATCH 286/358] chore(deps): bump the dependencies group with 2 updates Bumps the dependencies group with 2 updates: [github/ospo-reusable-workflows](https://github.com/github/ospo-reusable-workflows) and [github/codeql-action](https://github.com/github/codeql-action). Updates `github/ospo-reusable-workflows` from 0.4.6 to 0.5.0 - [Release notes](https://github.com/github/ospo-reusable-workflows/releases) - [Changelog](https://github.com/github/ospo-reusable-workflows/blob/main/docs/release-image.md) - [Commits](https://github.com/github/ospo-reusable-workflows/compare/ecdd405ebb379e0713e348440e6e26e85fc06773...10cfc2f9be5fce5e90150dfbffc7c0f4e68108ab) Updates `github/codeql-action` from 3.28.14 to 3.28.15 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/fc7e4a0fa01c3cca5fd6a1fddec5c0740c977aa2...45775bd8235c68ba998cffa5171334d58593da47) --- updated-dependencies: - dependency-name: github/ospo-reusable-workflows dependency-version: 0.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: github/codeql-action dependency-version: 3.28.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/auto-labeler.yml | 2 +- .github/workflows/pr-title.yml | 2 +- .github/workflows/release.yml | 6 +++--- .github/workflows/scorecard.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/auto-labeler.yml b/.github/workflows/auto-labeler.yml index f6da5ce5..eb586ccb 100644 --- a/.github/workflows/auto-labeler.yml +++ b/.github/workflows/auto-labeler.yml @@ -11,7 +11,7 @@ jobs: permissions: contents: write pull-requests: write - uses: github/ospo-reusable-workflows/.github/workflows/auto-labeler.yaml@ecdd405ebb379e0713e348440e6e26e85fc06773 + uses: github/ospo-reusable-workflows/.github/workflows/auto-labeler.yaml@10cfc2f9be5fce5e90150dfbffc7c0f4e68108ab with: config-name: release-drafter.yml secrets: diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index 0655a34d..e8d9e679 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -12,6 +12,6 @@ jobs: contents: read pull-requests: read statuses: write - uses: github/ospo-reusable-workflows/.github/workflows/pr-title.yaml@ecdd405ebb379e0713e348440e6e26e85fc06773 + uses: github/ospo-reusable-workflows/.github/workflows/pr-title.yaml@10cfc2f9be5fce5e90150dfbffc7c0f4e68108ab secrets: github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 48c53870..d0b95bcd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: permissions: contents: write pull-requests: read - uses: github/ospo-reusable-workflows/.github/workflows/release.yaml@ecdd405ebb379e0713e348440e6e26e85fc06773 + uses: github/ospo-reusable-workflows/.github/workflows/release.yaml@10cfc2f9be5fce5e90150dfbffc7c0f4e68108ab with: publish: true release-config-name: release-drafter.yml @@ -25,7 +25,7 @@ jobs: packages: write id-token: write attestations: write - uses: github/ospo-reusable-workflows/.github/workflows/release-image.yaml@ecdd405ebb379e0713e348440e6e26e85fc06773 + uses: github/ospo-reusable-workflows/.github/workflows/release-image.yaml@10cfc2f9be5fce5e90150dfbffc7c0f4e68108ab with: image-name: ${{ github.repository_owner }}/issue_metrics full-tag: ${{ needs.release.outputs.full-tag }} @@ -40,7 +40,7 @@ jobs: permissions: contents: read discussions: write - uses: github/ospo-reusable-workflows/.github/workflows/release-discussion.yaml@ecdd405ebb379e0713e348440e6e26e85fc06773 + uses: github/ospo-reusable-workflows/.github/workflows/release-discussion.yaml@10cfc2f9be5fce5e90150dfbffc7c0f4e68108ab with: full-tag: ${{ needs.release.outputs.full-tag }} body: ${{ needs.release.outputs.body }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index b74fbe18..df8ef00d 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@fc7e4a0fa01c3cca5fd6a1fddec5c0740c977aa2 # v3.24.9 + uses: github/codeql-action/upload-sarif@45775bd8235c68ba998cffa5171334d58593da47 # v3.24.9 with: sarif_file: results.sarif From 3765d396f4214baab8b55eb8c68391880d74bdcf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 17:55:35 +0000 Subject: [PATCH 287/358] chore(deps): bump python from `8f3aba4` to `21e39cf` Bumps python from `8f3aba4` to `21e39cf`. --- updated-dependencies: - dependency-name: python dependency-version: 3.13-slim dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e2d475cf..51d286bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.13-slim@sha256:8f3aba466a471c0ab903dbd7cb979abd4bda370b04789d25440cc90372b50e04 +FROM python:3.13-slim@sha256:21e39cf1815802d4c6f89a0d3a166cc67ce58f95b6d1639e68a394c99310d2e5 LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From d8bc64348ff29579ec571bc69a0a31f1ccef3a09 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 18:06:33 +0000 Subject: [PATCH 288/358] chore(deps): bump mypy-extensions in the dependencies group Bumps the dependencies group with 1 update: [mypy-extensions](https://github.com/python/mypy_extensions). Updates `mypy-extensions` from 1.0.0 to 1.1.0 - [Commits](https://github.com/python/mypy_extensions/compare/1.0.0...1.1.0) --- updated-dependencies: - dependency-name: mypy-extensions dependency-version: 1.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 7d6d42c6..de1d7818 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,7 +1,7 @@ black==25.1.0 flake8==7.2.0 mypy==1.15.0 -mypy-extensions==1.0.0 +mypy-extensions==1.1.0 pylint==3.3.6 pytest==8.3.5 pytest-cov==6.1.1 From 896dd5548223dfb54667180cdc747d4434180dac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 18:48:14 +0000 Subject: [PATCH 289/358] chore(deps): bump the dependencies group with 2 updates Bumps the dependencies group with 2 updates: [actions/setup-python](https://github.com/actions/setup-python) and [github/codeql-action](https://github.com/github/codeql-action). Updates `actions/setup-python` from 5.5.0 to 5.6.0 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v5.5.0...v5.6.0) Updates `github/codeql-action` from 3.28.15 to 3.28.16 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/45775bd8235c68ba998cffa5171334d58593da47...28deaeda66b76a05916b6923827895f2b14ab387) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: 5.6.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: github/codeql-action dependency-version: 3.28.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/linter.yaml | 2 +- .github/workflows/python-package.yml | 2 +- .github/workflows/scorecard.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index de76ffc9..94418b62 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -23,7 +23,7 @@ jobs: # Full git history is needed to get a proper # list of changed files within `super-linter` fetch-depth: 0 - - uses: actions/setup-python@v5.5.0 + - uses: actions/setup-python@v5.6.0 with: python-version: "3.12" - name: Install dependencies diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 5258d184..2853bb19 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -23,7 +23,7 @@ jobs: steps: - uses: actions/checkout@v4.2.2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5.5.0 + uses: actions/setup-python@v5.6.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index df8ef00d..75832975 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@45775bd8235c68ba998cffa5171334d58593da47 # v3.24.9 + uses: github/codeql-action/upload-sarif@28deaeda66b76a05916b6923827895f2b14ab387 # v3.24.9 with: sarif_file: results.sarif From d22ebb6d8f7e5539d0be6bbf13ff1fc366c421d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 17:32:39 +0000 Subject: [PATCH 290/358] chore(deps): bump pylint from 3.3.6 to 3.3.7 in the dependencies group Bumps the dependencies group with 1 update: [pylint](https://github.com/pylint-dev/pylint). Updates `pylint` from 3.3.6 to 3.3.7 - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v3.3.6...v3.3.7) --- updated-dependencies: - dependency-name: pylint dependency-version: 3.3.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index de1d7818..b6290652 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,7 +2,7 @@ black==25.1.0 flake8==7.2.0 mypy==1.15.0 mypy-extensions==1.1.0 -pylint==3.3.6 +pylint==3.3.7 pytest==8.3.5 pytest-cov==6.1.1 types-pytz==2025.2.0.20250326 From fedbb43cc1fdd2978fd2c68dc97dea3a16f85fd4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 17:43:29 +0000 Subject: [PATCH 291/358] chore(deps): bump the dependencies group with 2 updates Bumps the dependencies group with 2 updates: [github/contributors](https://github.com/github/contributors) and [github/codeql-action](https://github.com/github/codeql-action). Updates `github/contributors` from 1.5.6 to 1.5.7 - [Release notes](https://github.com/github/contributors/releases) - [Commits](https://github.com/github/contributors/compare/ed5a1f1c7c0038f4c9ad790685b6b8a549ab3f64...9e9ce8e51fe186c4d699ebb3d35c1f1fd755a357) Updates `github/codeql-action` from 3.28.16 to 3.28.17 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/28deaeda66b76a05916b6923827895f2b14ab387...60168efe1c415ce0f5521ea06d5c2062adbeed1b) --- updated-dependencies: - dependency-name: github/contributors dependency-version: 1.5.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: github/codeql-action dependency-version: 3.28.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/contributor_report.yaml | 2 +- .github/workflows/scorecard.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/contributor_report.yaml b/.github/workflows/contributor_report.yaml index 604a39e7..8c710420 100644 --- a/.github/workflows/contributor_report.yaml +++ b/.github/workflows/contributor_report.yaml @@ -27,7 +27,7 @@ jobs: echo "END_DATE=$end_date" >> "$GITHUB_ENV" - name: Run contributor action - uses: github/contributors@ed5a1f1c7c0038f4c9ad790685b6b8a549ab3f64 # v1.5.6 + uses: github/contributors@9e9ce8e51fe186c4d699ebb3d35c1f1fd755a357 # v1.5.7 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} START_DATE: ${{ env.START_DATE }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 75832975..5074dcd0 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@28deaeda66b76a05916b6923827895f2b14ab387 # v3.24.9 + uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.24.9 with: sarif_file: results.sarif From d746775d9354351c688d4e584c3f2b90c5f71cee Mon Sep 17 00:00:00 2001 From: jonasbreuer Date: Tue, 6 May 2025 13:32:22 +0200 Subject: [PATCH 292/358] feat: consider GH_ENTERPRISE_URL env in markdown author URL --- issue_metrics.py | 3 +++ test_markdown_writer.py | 13 ++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index ff3c6ca4..a1b3f116 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -284,6 +284,7 @@ def main(): # pragma: no cover non_mentioning_links=False, report_title=report_title, output_file=output_file, + ghe=ghe, ) return else: @@ -309,6 +310,7 @@ def main(): # pragma: no cover non_mentioning_links=False, report_title=report_title, output_file=output_file, + ghe=ghe, ) return @@ -371,6 +373,7 @@ def main(): # pragma: no cover non_mentioning_links=non_mentioning_links, report_title=report_title, output_file=output_file, + ghe=ghe, ) evaluate_markdown_file_size(output_file) diff --git a/test_markdown_writer.py b/test_markdown_writer.py index 1d63db51..a9259e65 100644 --- a/test_markdown_writer.py +++ b/test_markdown_writer.py @@ -298,10 +298,11 @@ def test_write_to_markdown_no_issues(self): "HIDE_TIME_TO_ANSWER": "True", "HIDE_LABEL_METRICS": "True", "NON_MENTIONING_LINKS": "True", + "GH_ENTERPRISE_URL": "https://github.mycompany.com", }, ) class TestWriteToMarkdownWithEnv(unittest.TestCase): - """Test the write_to_markdown function with the HIDE* and NON_MENTIONING_LINKS environment variables set.""" + """Test the write_to_markdown function with the HIDE*, NON_MENTIONING_LINKS and GH_ENTERPRISE_URL environment variables set.""" def test_writes_markdown_file_with_non_hidden_columns_only(self): """ @@ -313,7 +314,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): issues_with_metrics = [ IssueWithMetrics( title="Issue 1", - html_url="https://github.com/user/repo/issues/1", + html_url="https://github.mycompany.com/user/repo/issues/1", author="alice", created_at=timedelta(days=-5), time_to_first_response=timedelta(minutes=10), @@ -326,7 +327,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): ), IssueWithMetrics( title="Issue 2", - html_url="https://github.com/user/repo/issues/2", + html_url="https://github.mycompany.com/user/repo/issues/2", author="bob", created_at=timedelta(days=-5), time_to_first_response=timedelta(minutes=20), @@ -347,6 +348,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): num_issues_opened = 2 num_issues_closed = 2 num_mentor_count = 5 + ghe = "https://github.mycompany.com" # Call the function write_to_markdown( @@ -366,6 +368,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): non_mentioning_links=True, report_title="Issue Metrics", output_file="issue_metrics.md", + ghe=ghe, ) # Check that the function writes the correct markdown file @@ -381,8 +384,8 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): "| Total number of items created | 2 |\n\n" "| Title | URL | Author | Created At |\n" "| --- | --- | --- | --- |\n" - "| Issue 1 | https://www.github.com/user/repo/issues/1 | [alice](https://github.com/alice) | -5 days, 0:00:00 |\n" - "| Issue 2 | https://www.github.com/user/repo/issues/2 | [bob](https://github.com/bob) | -5 days, 0:00:00 |\n\n" + "| Issue 1 | https://www.github.mycompany.com/user/repo/issues/1 | [alice](https://github.mycompany.com/alice) | -5 days, 0:00:00 |\n" + "| Issue 2 | https://www.github.mycompany.com/user/repo/issues/2 | [bob](https://github.mycompany.com/bob) | -5 days, 0:00:00 |\n\n" "_This report was generated with the [Issue Metrics Action](https://github.com/github/issue-metrics)_\n" "Search query used to find these items: `repo:user/repo is:issue`\n" ) From 5477ea473302ac6da177eb47364cfd810fa747b6 Mon Sep 17 00:00:00 2001 From: jonasbreuer Date: Tue, 6 May 2025 14:27:31 +0200 Subject: [PATCH 293/358] style: fix linting issues in ghe-author-link feature --- test_markdown_writer.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/test_markdown_writer.py b/test_markdown_writer.py index a9259e65..3927106d 100644 --- a/test_markdown_writer.py +++ b/test_markdown_writer.py @@ -298,11 +298,15 @@ def test_write_to_markdown_no_issues(self): "HIDE_TIME_TO_ANSWER": "True", "HIDE_LABEL_METRICS": "True", "NON_MENTIONING_LINKS": "True", - "GH_ENTERPRISE_URL": "https://github.mycompany.com", + "GH_ENTERPRISE_URL": "https://ghe.com", }, ) class TestWriteToMarkdownWithEnv(unittest.TestCase): - """Test the write_to_markdown function with the HIDE*, NON_MENTIONING_LINKS and GH_ENTERPRISE_URL environment variables set.""" + """Test the write_to_markdown function with the following environment variables set: + - HIDE*, + - NON_MENTIONING_LINKS + - GH_ENTERPRISE_URL + """ def test_writes_markdown_file_with_non_hidden_columns_only(self): """ @@ -314,7 +318,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): issues_with_metrics = [ IssueWithMetrics( title="Issue 1", - html_url="https://github.mycompany.com/user/repo/issues/1", + html_url="https://ghe.com/user/repo/issues/1", author="alice", created_at=timedelta(days=-5), time_to_first_response=timedelta(minutes=10), @@ -327,7 +331,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): ), IssueWithMetrics( title="Issue 2", - html_url="https://github.mycompany.com/user/repo/issues/2", + html_url="https://ghe.com/user/repo/issues/2", author="bob", created_at=timedelta(days=-5), time_to_first_response=timedelta(minutes=20), @@ -348,7 +352,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): num_issues_opened = 2 num_issues_closed = 2 num_mentor_count = 5 - ghe = "https://github.mycompany.com" + ghe = "https://ghe.com" # Call the function write_to_markdown( @@ -384,8 +388,8 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): "| Total number of items created | 2 |\n\n" "| Title | URL | Author | Created At |\n" "| --- | --- | --- | --- |\n" - "| Issue 1 | https://www.github.mycompany.com/user/repo/issues/1 | [alice](https://github.mycompany.com/alice) | -5 days, 0:00:00 |\n" - "| Issue 2 | https://www.github.mycompany.com/user/repo/issues/2 | [bob](https://github.mycompany.com/bob) | -5 days, 0:00:00 |\n\n" + "| Issue 1 | https://www.ghe.com/user/repo/issues/1 | [alice](https://ghe.com/alice) | -5 days, 0:00:00 |\n" + "| Issue 2 | https://www.ghe.com/user/repo/issues/2 | [bob](https://ghe.com/bob) | -5 days, 0:00:00 |\n\n" "_This report was generated with the [Issue Metrics Action](https://github.com/github/issue-metrics)_\n" "Search query used to find these items: `repo:user/repo is:issue`\n" ) From 748a5f08c75c7aeee6ef12e8128c67c641d76112 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Thu, 8 May 2025 16:24:30 -0500 Subject: [PATCH 294/358] feat: pass through api error messages Signed-off-by: jmeridth Co-authored-by: Zack Koppert --- search.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/search.py b/search.py index 742d7e68..21091356 100644 --- a/search.py +++ b/search.py @@ -80,33 +80,50 @@ def wait_for_api_refresh( if idx % issues_per_page == 0: wait_for_api_refresh(issues_iterator, rate_limit_bypass) - except github3.exceptions.ForbiddenError: + except github3.exceptions.ForbiddenError as e: print( f"You do not have permission to view a repository \ from: '{repos_and_owners_string}'; Check your API Token." ) + print_error_messages(e) sys.exit(1) - except github3.exceptions.NotFoundError: + except github3.exceptions.NotFoundError as e: print( f"The repository could not be found; \ Check the repository owner and names: '{repos_and_owners_string}" ) + print_error_messages(e) sys.exit(1) - except github3.exceptions.ConnectionError: + except github3.exceptions.ConnectionError as e: print( "There was a connection error; Check your internet connection or API Token." ) + print_error_messages(e) sys.exit(1) - except github3.exceptions.AuthenticationFailed: + except github3.exceptions.AuthenticationFailed as e: print("Authentication failed; Check your API Token.") + print_error_messages(e) sys.exit(1) - except github3.exceptions.UnprocessableEntity: + except github3.exceptions.UnprocessableEntity as e: print("The search query is invalid; Check the search query.") + print_error_messages(e) sys.exit(1) return issues +def print_error_messages(error: github3.exceptions): + """Prints the error messages from the GitHub API response. + + Args: + Error (github3.exceptions): The error object from the GitHub API response. + + """ + if hasattr(error, "errors"): + for e in error.errors: + print(f"Error: {e.get('message')}") + + def get_owners_and_repositories( search_query: str, ) -> List[dict]: From 93ceb734055869deb17e5c0c7d455f847a7e6a8d Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Thu, 8 May 2025 22:12:51 +0000 Subject: [PATCH 295/358] fix: remove mentor count from stats if mentors are disabled Signed-off-by: Zack Koppert --- issue_metrics.py | 3 +++ markdown_writer.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/issue_metrics.py b/issue_metrics.py index a1b3f116..ab9cb916 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -281,6 +281,7 @@ def main(): # pragma: no cover search_query=search_query, hide_label_metrics=False, hide_items_closed_count=False, + enable_mentor_count=enable_mentor_count, non_mentioning_links=False, report_title=report_title, output_file=output_file, @@ -307,6 +308,7 @@ def main(): # pragma: no cover search_query=search_query, hide_label_metrics=False, hide_items_closed_count=False, + enable_mentor_count=enable_mentor_count, non_mentioning_links=False, report_title=report_title, output_file=output_file, @@ -370,6 +372,7 @@ def main(): # pragma: no cover search_query=search_query, hide_label_metrics=hide_label_metrics, hide_items_closed_count=hide_items_closed_count, + enable_mentor_count=enable_mentor_count, non_mentioning_links=non_mentioning_links, report_title=report_title, output_file=output_file, diff --git a/markdown_writer.py b/markdown_writer.py index e506a4f4..ed0d05d4 100644 --- a/markdown_writer.py +++ b/markdown_writer.py @@ -100,6 +100,7 @@ def write_to_markdown( search_query=None, hide_label_metrics=False, hide_items_closed_count=False, + enable_mentor_count=False, non_mentioning_links=False, report_title="", output_file="", @@ -168,6 +169,7 @@ def write_to_markdown( file, hide_label_metrics, hide_items_closed_count, + enable_mentor_count, ) # Write second table with individual issue/pr/discussion metrics @@ -243,6 +245,7 @@ def write_overall_metrics_tables( file, hide_label_metrics, hide_items_closed_count=False, + enable_mentor_count=False, ): """Write the overall metrics tables to the markdown file.""" if any( @@ -316,5 +319,6 @@ def write_overall_metrics_tables( file.write(f"| Number of items that remain open | {num_issues_opened} |\n") if not hide_items_closed_count: file.write(f"| Number of items closed | {num_issues_closed} |\n") - file.write(f"| Number of most active mentors | {num_mentor_count} |\n") + if enable_mentor_count: + file.write(f"| Number of most active mentors | {num_mentor_count} |\n") file.write(f"| Total number of items created | {len(issues_with_metrics)} |\n\n") From c45c5db5d6d8fbafdfff4bbae3d8950a110a0e76 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Thu, 8 May 2025 15:25:56 -0700 Subject: [PATCH 296/358] fix: fix tests to reflect when mentor count should and shouldnt be in the markdown table Signed-off-by: Zack Koppert --- test_markdown_writer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test_markdown_writer.py b/test_markdown_writer.py index 3927106d..e8f5b08d 100644 --- a/test_markdown_writer.py +++ b/test_markdown_writer.py @@ -129,7 +129,6 @@ def test_write_to_markdown(self): "| --- | ---: |\n" "| Number of items that remain open | 2 |\n" "| Number of items closed | 1 |\n" - "| Number of most active mentors | 5 |\n" "| Total number of items created | 2 |\n\n" "| Title | URL | Author | Time to first response | Time to close |" " Time to answer | Time in draft | Time spent in bug | Created At |\n" @@ -240,7 +239,6 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): "| --- | ---: |\n" "| Number of items that remain open | 2 |\n" "| Number of items closed | 1 |\n" - "| Number of most active mentors | 5 |\n" "| Total number of items created | 2 |\n\n" "| Title | URL | Author | Time to first response | Time to close |" " Time to answer | Time in draft | Time spent in bug | Created At |\n" @@ -369,6 +367,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): search_query="repo:user/repo is:issue", hide_label_metrics=True, hide_items_closed_count=True, + enable_mentor_count=True, non_mentioning_links=True, report_title="Issue Metrics", output_file="issue_metrics.md", From 992bf9db20811a4c35c4c49913926c5dde01bdde Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 17:48:47 +0000 Subject: [PATCH 297/358] chore(deps): bump github/contributors in the dependencies group Bumps the dependencies group with 1 update: [github/contributors](https://github.com/github/contributors). Updates `github/contributors` from 1.5.7 to 1.5.8 - [Release notes](https://github.com/github/contributors/releases) - [Commits](https://github.com/github/contributors/compare/9e9ce8e51fe186c4d699ebb3d35c1f1fd755a357...6949781e2a2575cba21a80325c9dd6014f5c898b) --- updated-dependencies: - dependency-name: github/contributors dependency-version: 1.5.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/contributor_report.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/contributor_report.yaml b/.github/workflows/contributor_report.yaml index 8c710420..1385546e 100644 --- a/.github/workflows/contributor_report.yaml +++ b/.github/workflows/contributor_report.yaml @@ -27,7 +27,7 @@ jobs: echo "END_DATE=$end_date" >> "$GITHUB_ENV" - name: Run contributor action - uses: github/contributors@9e9ce8e51fe186c4d699ebb3d35c1f1fd755a357 # v1.5.7 + uses: github/contributors@6949781e2a2575cba21a80325c9dd6014f5c898b # v1.5.8 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} START_DATE: ${{ env.START_DATE }} From 77d6a30b8655352dce42cc31ea8ceb3efe4cd312 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 18:03:00 +0000 Subject: [PATCH 298/358] chore(deps): bump python from `21e39cf` to `914bf5c` Bumps python from `21e39cf` to `914bf5c`. --- updated-dependencies: - dependency-name: python dependency-version: 3.13-slim dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 51d286bb..9f7709b2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.13-slim@sha256:21e39cf1815802d4c6f89a0d3a166cc67ce58f95b6d1639e68a394c99310d2e5 +FROM python:3.13-slim@sha256:914bf5c12ea40a97a78b2bff97fbdb766cc36ec903bfb4358faf2b74d73b555b LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From cbd7b777e7f774f8177edf5a891f5b051f38e961 Mon Sep 17 00:00:00 2001 From: Max Base Date: Tue, 13 May 2025 18:53:16 +0000 Subject: [PATCH 299/358] fix: occured -> occurred --- labels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/labels.py b/labels.py index 81b9b4b6..b9502b41 100644 --- a/labels.py +++ b/labels.py @@ -56,7 +56,7 @@ def get_label_metrics(issue: github3.issues.Issue, labels: List[str]) -> dict: # Calculate the time to add or subtract to the time spent in label based on the label events for event in label_events: - # Skip labeling events that have occured past issue close time + # Skip labeling events that have occurred past issue close time if issue.closed_at is not None and ( event.created_at >= datetime.fromisoformat(issue.closed_at) ): From d5b2446369861128eb25b2f1b76037047b9c47c9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 17:27:21 +0000 Subject: [PATCH 300/358] chore(deps): bump the dependencies group with 2 updates Bumps the dependencies group with 2 updates: [super-linter/super-linter](https://github.com/super-linter/super-linter) and [github/codeql-action](https://github.com/github/codeql-action). Updates `super-linter/super-linter` from 7.3.0 to 7.4.0 - [Release notes](https://github.com/super-linter/super-linter/releases) - [Changelog](https://github.com/super-linter/super-linter/blob/main/CHANGELOG.md) - [Commits](https://github.com/super-linter/super-linter/compare/4e8a7c2bf106c4c766c816b35ec612638dc9b6b2...12150456a73e248bdc94d0794898f94e23127c88) Updates `github/codeql-action` from 3.28.17 to 3.28.18 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/60168efe1c415ce0f5521ea06d5c2062adbeed1b...ff0a06e83cb2de871e5a09832bc6a81e7276941f) --- updated-dependencies: - dependency-name: super-linter/super-linter dependency-version: 7.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: github/codeql-action dependency-version: 3.28.18 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/linter.yaml | 2 +- .github/workflows/scorecard.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index 94418b62..9a7a3df4 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -30,7 +30,7 @@ jobs: run: | pip install -r requirements.txt -r requirements-test.txt - name: Lint Code Base - uses: super-linter/super-linter@4e8a7c2bf106c4c766c816b35ec612638dc9b6b2 # v7.3.0 + uses: super-linter/super-linter@12150456a73e248bdc94d0794898f94e23127c88 # v7.4.0 env: DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 5074dcd0..db4bd3c0 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.24.9 + uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.24.9 with: sarif_file: results.sarif From 350de6249a1f18ade8601fa7cb711b4e0129a933 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 17:38:06 +0000 Subject: [PATCH 301/358] chore(deps): bump types-requests from 2.32.0.20250328 to 2.32.0.20250515 Bumps [types-requests](https://github.com/typeshed-internal/stub_uploader) from 2.32.0.20250328 to 2.32.0.20250515. - [Commits](https://github.com/typeshed-internal/stub_uploader/commits) --- updated-dependencies: - dependency-name: types-requests dependency-version: 2.32.0.20250515 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index b6290652..919aec63 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -6,4 +6,4 @@ pylint==3.3.7 pytest==8.3.5 pytest-cov==6.1.1 types-pytz==2025.2.0.20250326 -types-requests==2.32.0.20250328 +types-requests==2.32.0.20250515 From 4dc710fcd784031740176699c03c6f0fbfea2c53 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 17:42:21 +0000 Subject: [PATCH 302/358] chore(deps): bump types-pytz from 2025.2.0.20250326 to 2025.2.0.20250516 Bumps [types-pytz](https://github.com/typeshed-internal/stub_uploader) from 2025.2.0.20250326 to 2025.2.0.20250516. - [Commits](https://github.com/typeshed-internal/stub_uploader/commits) --- updated-dependencies: - dependency-name: types-pytz dependency-version: 2025.2.0.20250516 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 919aec63..39847408 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -5,5 +5,5 @@ mypy-extensions==1.1.0 pylint==3.3.7 pytest==8.3.5 pytest-cov==6.1.1 -types-pytz==2025.2.0.20250326 +types-pytz==2025.2.0.20250516 types-requests==2.32.0.20250515 From 3d10a9f519dd1a82cd1c92d319cfa4590a27a646 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 May 2025 17:51:01 +0000 Subject: [PATCH 303/358] chore(deps): bump python from `914bf5c` to `56a1136` Bumps python from `914bf5c` to `56a1136`. --- updated-dependencies: - dependency-name: python dependency-version: 3.13-slim dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9f7709b2..dd3fc581 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.13-slim@sha256:914bf5c12ea40a97a78b2bff97fbdb766cc36ec903bfb4358faf2b74d73b555b +FROM python:3.13-slim@sha256:56a11364ffe0fee3bd60af6d6d5209eba8a99c2c16dc4c7c5861dc06261503cc LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From edaaaf3017cf74343d10bbf59fb4b366ecdae8e3 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Sat, 31 May 2025 16:13:21 -0500 Subject: [PATCH 304/358] fix: docker image content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - [x] add .dockerignore to ensure non application files/test files don't get into the container image Tested locally with the following commands: ```bash ➜ docker build . --tag test:1 ➜ docker run -it --rm --name debug --entrypoint /bin/bash test:1 ## inside container root@3624b051f2c8:/action/workspace# ls -lah total 44K drwxr-xr-x 1 root root 4.0K May 27 13:11 . drwxr-xr-x 1 root root 4.0K May 27 13:11 .. -rw-r--r-- 1 root root 1.8K Jan 7 10:26 auth.py -rw-r--r-- 1 root root 4.4K Mar 30 23:25 env.py -rw-r--r-- 1 root root 68 Apr 4 04:04 requirements.txt -rwxr-xr-x 1 root root 14K May 27 12:11 stale_repos.py ``` Signed-off-by: jmeridth --- .dockerignore | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..1f934fb3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,36 @@ +# Application specific files +test_*.py + +# Python +*.pyc +__pycache__/ +*.pyo +*.pyd + +# Common +*.md +docker-compose.yml +Dockerfile* +.env* +Makefile + +# Logs +logs +*.log + +# IDEs +.vscode/ +.idea/ + +# Dependency directories +node_modules/ +.venv/ + +## Cache directories +.parcel-cache + +# git +.git +.gitattributes +.gitignore +.github/ From 7eccff2cd27235390c125c3f04d6a2bb2aabf1a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 17:52:12 +0000 Subject: [PATCH 305/358] chore(deps): bump the dependencies group with 2 updates Bumps the dependencies group with 2 updates: [mypy](https://github.com/python/mypy) and [pytest](https://github.com/pytest-dev/pytest). Updates `mypy` from 1.15.0 to 1.16.0 - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.15.0...v1.16.0) Updates `pytest` from 8.3.5 to 8.4.0 - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.3.5...8.4.0) --- updated-dependencies: - dependency-name: mypy dependency-version: 1.16.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: pytest dependency-version: 8.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index 39847408..1e4aab5b 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,9 +1,9 @@ black==25.1.0 flake8==7.2.0 -mypy==1.15.0 +mypy==1.16.0 mypy-extensions==1.1.0 pylint==3.3.7 -pytest==8.3.5 +pytest==8.4.0 pytest-cov==6.1.1 types-pytz==2025.2.0.20250516 types-requests==2.32.0.20250515 From dd30bb3f9fd8029a5d09d7354944461475cf9a75 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 18:08:04 +0000 Subject: [PATCH 306/358] chore(deps): bump the dependencies group with 2 updates Bumps the dependencies group with 2 updates: [github/ospo-reusable-workflows](https://github.com/github/ospo-reusable-workflows) and [ossf/scorecard-action](https://github.com/ossf/scorecard-action). Updates `github/ospo-reusable-workflows` from 0.5.0 to 0.5.1 - [Release notes](https://github.com/github/ospo-reusable-workflows/releases) - [Changelog](https://github.com/github/ospo-reusable-workflows/blob/main/docs/release-image.md) - [Commits](https://github.com/github/ospo-reusable-workflows/compare/10cfc2f9be5fce5e90150dfbffc7c0f4e68108ab...6f158f242fe68adb5a2698ef47e06dac07ac7e71) Updates `ossf/scorecard-action` from 2.4.1 to 2.4.2 - [Release notes](https://github.com/ossf/scorecard-action/releases) - [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md) - [Commits](https://github.com/ossf/scorecard-action/compare/f49aabe0b5af0936a0987cfb85d86b75731b0186...05b42c624433fc40578a4040d5cf5e36ddca8cde) --- updated-dependencies: - dependency-name: github/ospo-reusable-workflows dependency-version: 0.5.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: ossf/scorecard-action dependency-version: 2.4.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/auto-labeler.yml | 2 +- .github/workflows/pr-title.yml | 2 +- .github/workflows/release.yml | 6 +++--- .github/workflows/scorecard.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/auto-labeler.yml b/.github/workflows/auto-labeler.yml index eb586ccb..43fe7790 100644 --- a/.github/workflows/auto-labeler.yml +++ b/.github/workflows/auto-labeler.yml @@ -11,7 +11,7 @@ jobs: permissions: contents: write pull-requests: write - uses: github/ospo-reusable-workflows/.github/workflows/auto-labeler.yaml@10cfc2f9be5fce5e90150dfbffc7c0f4e68108ab + uses: github/ospo-reusable-workflows/.github/workflows/auto-labeler.yaml@6f158f242fe68adb5a2698ef47e06dac07ac7e71 with: config-name: release-drafter.yml secrets: diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index e8d9e679..e06a1fbb 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -12,6 +12,6 @@ jobs: contents: read pull-requests: read statuses: write - uses: github/ospo-reusable-workflows/.github/workflows/pr-title.yaml@10cfc2f9be5fce5e90150dfbffc7c0f4e68108ab + uses: github/ospo-reusable-workflows/.github/workflows/pr-title.yaml@6f158f242fe68adb5a2698ef47e06dac07ac7e71 secrets: github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d0b95bcd..2f89b0a2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: permissions: contents: write pull-requests: read - uses: github/ospo-reusable-workflows/.github/workflows/release.yaml@10cfc2f9be5fce5e90150dfbffc7c0f4e68108ab + uses: github/ospo-reusable-workflows/.github/workflows/release.yaml@6f158f242fe68adb5a2698ef47e06dac07ac7e71 with: publish: true release-config-name: release-drafter.yml @@ -25,7 +25,7 @@ jobs: packages: write id-token: write attestations: write - uses: github/ospo-reusable-workflows/.github/workflows/release-image.yaml@10cfc2f9be5fce5e90150dfbffc7c0f4e68108ab + uses: github/ospo-reusable-workflows/.github/workflows/release-image.yaml@6f158f242fe68adb5a2698ef47e06dac07ac7e71 with: image-name: ${{ github.repository_owner }}/issue_metrics full-tag: ${{ needs.release.outputs.full-tag }} @@ -40,7 +40,7 @@ jobs: permissions: contents: read discussions: write - uses: github/ospo-reusable-workflows/.github/workflows/release-discussion.yaml@10cfc2f9be5fce5e90150dfbffc7c0f4e68108ab + uses: github/ospo-reusable-workflows/.github/workflows/release-discussion.yaml@6f158f242fe68adb5a2698ef47e06dac07ac7e71 with: full-tag: ${{ needs.release.outputs.full-tag }} body: ${{ needs.release.outputs.body }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index db4bd3c0..4d486926 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 + uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 with: results_file: results.sarif results_format: sarif From 3c0141405f594eacb55f806f081d7bcbbc21ad69 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 18:09:55 +0000 Subject: [PATCH 307/358] chore(deps): bump types-requests from 2.32.0.20250515 to 2.32.0.20250602 Bumps [types-requests](https://github.com/typeshed-internal/stub_uploader) from 2.32.0.20250515 to 2.32.0.20250602. - [Commits](https://github.com/typeshed-internal/stub_uploader/commits) --- updated-dependencies: - dependency-name: types-requests dependency-version: 2.32.0.20250602 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 1e4aab5b..72949275 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -6,4 +6,4 @@ pylint==3.3.7 pytest==8.4.0 pytest-cov==6.1.1 types-pytz==2025.2.0.20250516 -types-requests==2.32.0.20250515 +types-requests==2.32.0.20250602 From ad20da8c568950e322bb8cb4227f78f5cce7d783 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Jun 2025 17:33:41 +0000 Subject: [PATCH 308/358] chore(deps): bump github/codeql-action in the dependencies group Bumps the dependencies group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 3.28.18 to 3.28.19 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/ff0a06e83cb2de871e5a09832bc6a81e7276941f...fca7ace96b7d713c7035871441bd52efbe39e27e) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.28.19 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 4d486926..cfebfa81 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.24.9 + uses: github/codeql-action/upload-sarif@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.24.9 with: sarif_file: results.sarif From b6cd64bc93c4792302a5c0dc15e4251479f4ca94 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Jun 2025 18:09:40 +0000 Subject: [PATCH 309/358] chore(deps): bump requests in the dependencies group Bumps the dependencies group with 1 update: [requests](https://github.com/psf/requests). Updates `requests` from 2.32.3 to 2.32.4 - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.32.3...v2.32.4) --- updated-dependencies: - dependency-name: requests dependency-version: 2.32.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a2f09048..efe5a96e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ github3.py==4.0.1 numpy==2.2.4 python-dotenv==1.1.0 pytz==2025.2 -requests==2.32.3 +requests==2.32.4 From e3d28fb4e19fc260a985a37460dc292c1b8a446f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Jun 2025 23:10:05 +0000 Subject: [PATCH 310/358] chore(deps): bump python from `56a1136` to `d97b595` Bumps python from `56a1136` to `d97b595`. --- updated-dependencies: - dependency-name: python dependency-version: 3.13-slim dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index dd3fc581..22d76a14 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.13-slim@sha256:56a11364ffe0fee3bd60af6d6d5209eba8a99c2c16dc4c7c5861dc06261503cc +FROM python:3.13-slim@sha256:d97b595c5f4ac718102e5a5a91adaf04b22e852961a698411637c718d45867c8 LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From d4a37472e1cf0905e422ad0fcbf24e101b7d2e1b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Jun 2025 13:45:11 +0000 Subject: [PATCH 311/358] Initial plan for issue From be2f85c45eaeadfcea57cc4a6b6e2895b159738c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Jun 2025 14:02:15 +0000 Subject: [PATCH 312/358] Add assignee support to issue metrics reporting Co-authored-by: zkoppert <6935431+zkoppert@users.noreply.github.com> --- classes.py | 6 +++ config.py | 6 +++ issue_metrics.py | 17 ++++++ json_writer.py | 2 + markdown_writer.py | 9 ++++ test_assignee_functionality.py | 96 ++++++++++++++++++++++++++++++++++ test_config.py | 4 ++ test_json_writer.py | 16 ++++++ test_markdown_writer.py | 36 ++++++++----- 9 files changed, 180 insertions(+), 12 deletions(-) create mode 100644 test_assignee_functionality.py diff --git a/classes.py b/classes.py index 414ab828..20ab9b37 100644 --- a/classes.py +++ b/classes.py @@ -13,6 +13,8 @@ class IssueWithMetrics: title (str): The title of the issue. html_url (str): The URL of the issue on GitHub. author (str): The author of the issue. + assignee (str, optional): The primary assignee of the issue. + assignees (list, optional): All assignees of the issue. time_to_first_response (timedelta, optional): The time it took to get the first response to the issue. time_to_close (timedelta, optional): The time it took to close the issue. @@ -38,10 +40,14 @@ def __init__( labels_metrics=None, mentor_activity=None, created_at=None, + assignee=None, + assignees=None, ): self.title = title self.html_url = html_url self.author = author + self.assignee = assignee + self.assignees = assignees or [] self.time_to_first_response = time_to_first_response self.time_to_close = time_to_close self.time_to_answer = time_to_answer diff --git a/config.py b/config.py index 38a7353c..55768dce 100644 --- a/config.py +++ b/config.py @@ -30,6 +30,7 @@ class EnvVars: authentication gh_token (str | None): GitHub personal access token (PAT) for API authentication ghe (str): The GitHub Enterprise URL to use for authentication + hide_assignee (bool): If true, the assignee's information is hidden in the output hide_author (bool): If true, the author's information is hidden in the output hide_items_closed_count (bool): If true, the number of items closed metric is hidden in the output @@ -64,6 +65,7 @@ def __init__( gh_app_enterprise_only: bool, gh_token: str | None, ghe: str | None, + hide_assignee: bool, hide_author: bool, hide_items_closed_count: bool, hide_label_metrics: bool, @@ -92,6 +94,7 @@ def __init__( self.ghe = ghe self.ignore_users = ignore_user self.labels_to_measure = labels_to_measure + self.hide_assignee = hide_assignee self.hide_author = hide_author self.hide_items_closed_count = hide_items_closed_count self.hide_label_metrics = hide_label_metrics @@ -119,6 +122,7 @@ def __repr__(self): f"{self.gh_app_enterprise_only}," f"{self.gh_token}," f"{self.ghe}," + f"{self.hide_assignee}," f"{self.hide_author}," f"{self.hide_items_closed_count})," f"{self.hide_label_metrics}," @@ -226,6 +230,7 @@ def get_env_vars(test: bool = False) -> EnvVars: draft_pr_tracking = get_bool_env_var("DRAFT_PR_TRACKING", False) # Hidden columns + hide_assignee = get_bool_env_var("HIDE_ASSIGNEE", False) hide_author = get_bool_env_var("HIDE_AUTHOR", False) hide_items_closed_count = get_bool_env_var("HIDE_ITEMS_CLOSED_COUNT", False) hide_label_metrics = get_bool_env_var("HIDE_LABEL_METRICS", False) @@ -246,6 +251,7 @@ def get_env_vars(test: bool = False) -> EnvVars: gh_app_enterprise_only, gh_token, ghe, + hide_assignee, hide_author, hide_items_closed_count, hide_label_metrics, diff --git a/issue_metrics.py b/issue_metrics.py index ab9cb916..f36a0f69 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -85,6 +85,9 @@ def get_per_issue_metrics( None, None, ) + # Discussions typically don't have assignees in the same way as issues/PRs + issue_with_metrics.assignee = None + issue_with_metrics.assignees = [] if env_vars.hide_time_to_first_response is False: issue_with_metrics.time_to_first_response = ( measure_time_to_first_response(None, issue, ignore_users) @@ -119,6 +122,20 @@ def get_per_issue_metrics( author=issue.user["login"], # type: ignore ) + # Extract assignee information from the issue + issue_dict = issue.issue.as_dict() # type: ignore + assignee = None + assignees = [] + + if issue_dict.get("assignee"): + assignee = issue_dict["assignee"]["login"] + + if issue_dict.get("assignees"): + assignees = [a["login"] for a in issue_dict["assignees"]] + + issue_with_metrics.assignee = assignee + issue_with_metrics.assignees = assignees + # Check if issue is actually a pull request pull_request, ready_for_review_at = None, None if issue.issue.pull_request_urls: # type: ignore diff --git a/json_writer.py b/json_writer.py index 6e388968..11285601 100644 --- a/json_writer.py +++ b/json_writer.py @@ -177,6 +177,8 @@ def write_to_json( "title": issue.title, "html_url": issue.html_url, "author": issue.author, + "assignee": issue.assignee, + "assignees": issue.assignees, "time_to_first_response": str(issue.time_to_first_response), "time_to_close": str(issue.time_to_close), "time_to_answer": str(issue.time_to_answer), diff --git a/markdown_writer.py b/markdown_writer.py index ed0d05d4..500dc0a0 100644 --- a/markdown_writer.py +++ b/markdown_writer.py @@ -55,6 +55,10 @@ def get_non_hidden_columns(labels) -> List[str]: env_vars = get_env_vars() # Find the number of columns and which are to be hidden + hide_assignee = env_vars.hide_assignee + if not hide_assignee: + columns.append("Assignee") + hide_author = env_vars.hide_author if not hide_author: columns.append("Author") @@ -203,6 +207,11 @@ def write_to_markdown( ) else: file.write(f"| {issue.title} | {issue.html_url} |") + if "Assignee" in columns: + if issue.assignee: + file.write(f" [{issue.assignee}](https://{endpoint}/{issue.assignee}) |") + else: + file.write(" None |") if "Author" in columns: file.write(f" [{issue.author}](https://{endpoint}/{issue.author}) |") if "Time to first response" in columns: diff --git a/test_assignee_functionality.py b/test_assignee_functionality.py new file mode 100644 index 00000000..cbf2d14c --- /dev/null +++ b/test_assignee_functionality.py @@ -0,0 +1,96 @@ +"""Test assignee functionality added to issue metrics.""" + +import os +import unittest +from unittest.mock import patch +from markdown_writer import get_non_hidden_columns + + +class TestAssigneeFunctionality(unittest.TestCase): + """Test suite for the assignee functionality.""" + + @patch.dict( + os.environ, + { + "GH_TOKEN": "test_token", + "SEARCH_QUERY": "is:issue is:open repo:user/repo", + "HIDE_ASSIGNEE": "false", + "HIDE_AUTHOR": "false", + }, + clear=True, + ) + def test_get_non_hidden_columns_includes_assignee_by_default(self): + """Test that assignee column is included by default.""" + columns = get_non_hidden_columns(labels=None) + self.assertIn("Assignee", columns) + self.assertIn("Author", columns) + + @patch.dict( + os.environ, + { + "GH_TOKEN": "test_token", + "SEARCH_QUERY": "is:issue is:open repo:user/repo", + "HIDE_ASSIGNEE": "true", + "HIDE_AUTHOR": "false", + }, + clear=True, + ) + def test_get_non_hidden_columns_hides_assignee_when_env_set(self): + """Test that assignee column is hidden when HIDE_ASSIGNEE is true.""" + columns = get_non_hidden_columns(labels=None) + self.assertNotIn("Assignee", columns) + self.assertIn("Author", columns) + + @patch.dict( + os.environ, + { + "GH_TOKEN": "test_token", + "SEARCH_QUERY": "is:issue is:open repo:user/repo", + "HIDE_ASSIGNEE": "false", + "HIDE_AUTHOR": "true", + }, + clear=True, + ) + def test_get_non_hidden_columns_shows_assignee_but_hides_author(self): + """Test that assignee can be shown while author is hidden.""" + columns = get_non_hidden_columns(labels=None) + self.assertIn("Assignee", columns) + self.assertNotIn("Author", columns) + + @patch.dict( + os.environ, + { + "GH_TOKEN": "test_token", + "SEARCH_QUERY": "is:issue is:open repo:user/repo", + "HIDE_ASSIGNEE": "true", + "HIDE_AUTHOR": "true", + }, + clear=True, + ) + def test_get_non_hidden_columns_hides_both_assignee_and_author(self): + """Test that both assignee and author can be hidden.""" + columns = get_non_hidden_columns(labels=None) + self.assertNotIn("Assignee", columns) + self.assertNotIn("Author", columns) + + def test_assignee_column_position(self): + """Test that assignee column appears before author column.""" + with patch.dict( + os.environ, + { + "GH_TOKEN": "test_token", + "SEARCH_QUERY": "is:issue is:open repo:user/repo", + "HIDE_ASSIGNEE": "false", + "HIDE_AUTHOR": "false", + }, + clear=True, + ): + columns = get_non_hidden_columns(labels=None) + assignee_index = columns.index("Assignee") + author_index = columns.index("Author") + self.assertLess(assignee_index, author_index, + "Assignee column should appear before Author column") + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/test_config.py b/test_config.py index 327f851a..537d1573 100644 --- a/test_config.py +++ b/test_config.py @@ -123,6 +123,7 @@ def test_get_env_vars_with_github_app(self): gh_app_enterprise_only=False, gh_token="", ghe="", + hide_assignee=False, hide_author=False, hide_items_closed_count=False, hide_label_metrics=False, @@ -177,6 +178,7 @@ def test_get_env_vars_with_token(self): gh_app_enterprise_only=False, gh_token=TOKEN, ghe="", + hide_assignee=False, hide_author=False, hide_items_closed_count=False, hide_label_metrics=False, @@ -266,6 +268,7 @@ def test_get_env_vars_optional_values(self): gh_app_enterprise_only=False, gh_token=TOKEN, ghe="", + hide_assignee=False, hide_author=True, hide_items_closed_count=True, hide_label_metrics=True, @@ -309,6 +312,7 @@ def test_get_env_vars_optionals_are_defaulted(self): gh_app_enterprise_only=False, gh_token="TOKEN", ghe="", + hide_assignee=False, hide_author=False, hide_items_closed_count=False, hide_label_metrics=False, diff --git a/test_json_writer.py b/test_json_writer.py index 02278dfe..3ace4193 100644 --- a/test_json_writer.py +++ b/test_json_writer.py @@ -21,6 +21,8 @@ def test_write_to_json(self): title="Issue 1", html_url="https://github.com/owner/repo/issues/1", author="alice", + assignee="charlie", + assignees=["charlie"], time_to_first_response=timedelta(days=3), time_to_close=timedelta(days=6), time_to_answer=None, @@ -34,6 +36,8 @@ def test_write_to_json(self): title="Issue 2", html_url="https://github.com/owner/repo/issues/2", author="bob", + assignee=None, + assignees=[], time_to_first_response=timedelta(days=2), time_to_close=timedelta(days=4), time_to_answer=timedelta(days=1), @@ -96,6 +100,8 @@ def test_write_to_json(self): "title": "Issue 1", "html_url": "https://github.com/owner/repo/issues/1", "author": "alice", + "assignee": "charlie", + "assignees": ["charlie"], "time_to_first_response": "3 days, 0:00:00", "time_to_close": "6 days, 0:00:00", "time_to_answer": "None", @@ -107,6 +113,8 @@ def test_write_to_json(self): "title": "Issue 2", "html_url": "https://github.com/owner/repo/issues/2", "author": "bob", + "assignee": None, + "assignees": [], "time_to_first_response": "2 days, 0:00:00", "time_to_close": "4 days, 0:00:00", "time_to_answer": "1 day, 0:00:00", @@ -143,6 +151,8 @@ def test_write_to_json_with_no_response(self): title="Issue 1", html_url="https://github.com/owner/repo/issues/1", author="alice", + assignee=None, + assignees=[], time_to_first_response=None, time_to_close=None, time_to_answer=None, @@ -153,6 +163,8 @@ def test_write_to_json_with_no_response(self): title="Issue 2", html_url="https://github.com/owner/repo/issues/2", author="bob", + assignee=None, + assignees=[], time_to_first_response=None, time_to_close=None, time_to_answer=None, @@ -199,6 +211,8 @@ def test_write_to_json_with_no_response(self): "title": "Issue 1", "html_url": "https://github.com/owner/repo/issues/1", "author": "alice", + "assignee": None, + "assignees": [], "time_to_first_response": "None", "time_to_close": "None", "time_to_answer": "None", @@ -210,6 +224,8 @@ def test_write_to_json_with_no_response(self): "title": "Issue 2", "html_url": "https://github.com/owner/repo/issues/2", "author": "bob", + "assignee": None, + "assignees": [], "time_to_first_response": "None", "time_to_close": "None", "time_to_answer": "None", diff --git a/test_markdown_writer.py b/test_markdown_writer.py index e8f5b08d..4c3b9b2a 100644 --- a/test_markdown_writer.py +++ b/test_markdown_writer.py @@ -45,6 +45,8 @@ def test_write_to_markdown(self): title="Issue 1", html_url="https://github.com/user/repo/issues/1", author="alice", + assignee="charlie", + assignees=["charlie"], created_at=timedelta(days=-5), time_to_first_response=timedelta(days=1), time_to_close=timedelta(days=2), @@ -56,6 +58,8 @@ def test_write_to_markdown(self): title="Issue 2\r", html_url="https://github.com/user/repo/issues/2", author="bob", + assignee=None, + assignees=[], created_at=timedelta(days=-5), time_to_first_response=timedelta(days=3), time_to_close=timedelta(days=4), @@ -130,12 +134,12 @@ def test_write_to_markdown(self): "| Number of items that remain open | 2 |\n" "| Number of items closed | 1 |\n" "| Total number of items created | 2 |\n\n" - "| Title | URL | Author | Time to first response | Time to close |" + "| Title | URL | Assignee | Author | Time to first response | Time to close |" " Time to answer | Time in draft | Time spent in bug | Created At |\n" - "| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n" - "| Issue 1 | https://github.com/user/repo/issues/1 | [alice](https://github.com/alice) | 1 day, 0:00:00 | " + "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n" + "| Issue 1 | https://github.com/user/repo/issues/1 | [charlie](https://github.com/charlie) | [alice](https://github.com/alice) | 1 day, 0:00:00 | " "2 days, 0:00:00 | 3 days, 0:00:00 | 1 day, 0:00:00 | 4 days, 0:00:00 | -5 days, 0:00:00 |\n" - "| Issue 2 | https://github.com/user/repo/issues/2 | [bob](https://github.com/bob) | 3 days, 0:00:00 | " + "| Issue 2 | https://github.com/user/repo/issues/2 | None | [bob](https://github.com/bob) | 3 days, 0:00:00 | " "4 days, 0:00:00 | 5 days, 0:00:00 | 1 day, 0:00:00 | 2 days, 0:00:00 | -5 days, 0:00:00 |\n\n" "_This report was generated with the [Issue Metrics Action](https://github.com/github/issue-metrics)_\n" "Search query used to find these items: `is:issue is:open label:bug`\n" @@ -158,6 +162,8 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): title="Issue 1", html_url="https://github.com/user/repo/issues/1", author="alice", + assignee="charlie", + assignees=["charlie"], created_at=timedelta(days=-5), time_to_first_response=timedelta(days=1), time_to_close=timedelta(days=2), @@ -169,6 +175,8 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): title="feat| Issue 2", # title contains a vertical bar html_url="https://github.com/user/repo/issues/2", author="bob", + assignee=None, + assignees=[], created_at=timedelta(days=-5), time_to_first_response=timedelta(days=3), time_to_close=timedelta(days=4), @@ -240,12 +248,12 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): "| Number of items that remain open | 2 |\n" "| Number of items closed | 1 |\n" "| Total number of items created | 2 |\n\n" - "| Title | URL | Author | Time to first response | Time to close |" + "| Title | URL | Assignee | Author | Time to first response | Time to close |" " Time to answer | Time in draft | Time spent in bug | Created At |\n" - "| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n" - "| Issue 1 | https://github.com/user/repo/issues/1 | [alice](https://github.com/alice) | 1 day, 0:00:00 | " + "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n" + "| Issue 1 | https://github.com/user/repo/issues/1 | [charlie](https://github.com/charlie) | [alice](https://github.com/alice) | 1 day, 0:00:00 | " "2 days, 0:00:00 | 3 days, 0:00:00 | 1 day, 0:00:00 | 1 day, 0:00:00 | -5 days, 0:00:00 |\n" - "| feat| Issue 2 | https://github.com/user/repo/issues/2 | [bob](https://github.com/bob) | 3 days, 0:00:00 | " + "| feat| Issue 2 | https://github.com/user/repo/issues/2 | None | [bob](https://github.com/bob) | 3 days, 0:00:00 | " "4 days, 0:00:00 | 5 days, 0:00:00 | None | 2 days, 0:00:00 | -5 days, 0:00:00 |\n\n" "_This report was generated with the [Issue Metrics Action](https://github.com/github/issue-metrics)_\n" ) @@ -318,6 +326,8 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): title="Issue 1", html_url="https://ghe.com/user/repo/issues/1", author="alice", + assignee="charlie", + assignees=["charlie"], created_at=timedelta(days=-5), time_to_first_response=timedelta(minutes=10), time_to_close=timedelta(days=1), @@ -331,6 +341,8 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): title="Issue 2", html_url="https://ghe.com/user/repo/issues/2", author="bob", + assignee=None, + assignees=[], created_at=timedelta(days=-5), time_to_first_response=timedelta(minutes=20), time_to_close=timedelta(days=2), @@ -385,10 +397,10 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): "| Number of items that remain open | 2 |\n" "| Number of most active mentors | 5 |\n" "| Total number of items created | 2 |\n\n" - "| Title | URL | Author | Created At |\n" - "| --- | --- | --- | --- |\n" - "| Issue 1 | https://www.ghe.com/user/repo/issues/1 | [alice](https://ghe.com/alice) | -5 days, 0:00:00 |\n" - "| Issue 2 | https://www.ghe.com/user/repo/issues/2 | [bob](https://ghe.com/bob) | -5 days, 0:00:00 |\n\n" + "| Title | URL | Assignee | Author | Created At |\n" + "| --- | --- | --- | --- | --- |\n" + "| Issue 1 | https://www.ghe.com/user/repo/issues/1 | [charlie](https://ghe.com/charlie) | [alice](https://ghe.com/alice) | -5 days, 0:00:00 |\n" + "| Issue 2 | https://www.ghe.com/user/repo/issues/2 | None | [bob](https://ghe.com/bob) | -5 days, 0:00:00 |\n\n" "_This report was generated with the [Issue Metrics Action](https://github.com/github/issue-metrics)_\n" "Search query used to find these items: `repo:user/repo is:issue`\n" ) From 035687d893e0a5f60cd111f0ab04fb5f78edc117 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Jun 2025 14:04:59 +0000 Subject: [PATCH 313/358] Clean up code formatting and add integration tests Co-authored-by: zkoppert <6935431+zkoppert@users.noreply.github.com> --- issue_metrics.py | 6 +- markdown_writer.py | 9 +- test_assignee_integration.py | 156 +++++++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 5 deletions(-) create mode 100644 test_assignee_integration.py diff --git a/issue_metrics.py b/issue_metrics.py index f36a0f69..e7f59820 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -126,13 +126,13 @@ def get_per_issue_metrics( issue_dict = issue.issue.as_dict() # type: ignore assignee = None assignees = [] - + if issue_dict.get("assignee"): assignee = issue_dict["assignee"]["login"] - + if issue_dict.get("assignees"): assignees = [a["login"] for a in issue_dict["assignees"]] - + issue_with_metrics.assignee = assignee issue_with_metrics.assignees = assignees diff --git a/markdown_writer.py b/markdown_writer.py index 500dc0a0..7ff3a833 100644 --- a/markdown_writer.py +++ b/markdown_writer.py @@ -209,11 +209,16 @@ def write_to_markdown( file.write(f"| {issue.title} | {issue.html_url} |") if "Assignee" in columns: if issue.assignee: - file.write(f" [{issue.assignee}](https://{endpoint}/{issue.assignee}) |") + file.write( + f" [{issue.assignee}](https://{endpoint}/" + f"{issue.assignee}) |" + ) else: file.write(" None |") if "Author" in columns: - file.write(f" [{issue.author}](https://{endpoint}/{issue.author}) |") + file.write( + f" [{issue.author}](https://{endpoint}/{issue.author}) |" + ) if "Time to first response" in columns: file.write(f" {issue.time_to_first_response} |") if "Time to close" in columns: diff --git a/test_assignee_integration.py b/test_assignee_integration.py new file mode 100644 index 00000000..8601484f --- /dev/null +++ b/test_assignee_integration.py @@ -0,0 +1,156 @@ +"""Integration test for assignee functionality.""" + +import json +import os +import tempfile +import unittest +from unittest.mock import MagicMock, patch +from datetime import datetime, timedelta +from classes import IssueWithMetrics +from markdown_writer import write_to_markdown +from json_writer import write_to_json + + +class TestAssigneeIntegration(unittest.TestCase): + """Integration test for assignee functionality.""" + + @patch.dict( + os.environ, + { + "GH_TOKEN": "test_token", + "SEARCH_QUERY": "repo:test/repo is:issue", + }, + clear=True, + ) + def test_assignee_in_markdown_output(self): + """Test that assignee information appears correctly in markdown output.""" + issues_with_metrics = [ + IssueWithMetrics( + title="Test Issue 1", + html_url="https://github.com/test/repo/issues/1", + author="john", + assignee="alice", + assignees=["alice"], + time_to_first_response=timedelta(hours=2), + time_to_close=timedelta(days=1), + created_at=datetime.now() - timedelta(days=2), + ), + IssueWithMetrics( + title="Test Issue 2", + html_url="https://github.com/test/repo/issues/2", + author="jane", + assignee=None, + assignees=[], + time_to_first_response=timedelta(hours=4), + time_to_close=None, + created_at=datetime.now() - timedelta(days=1), + ), + ] + + with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f: + output_file = f.name + + try: + write_to_markdown( + issues_with_metrics=issues_with_metrics, + average_time_to_first_response={"avg": timedelta(hours=3), "med": timedelta(hours=3), "90p": timedelta(hours=4)}, + average_time_to_close={"avg": timedelta(days=1), "med": timedelta(days=1), "90p": timedelta(days=1)}, + average_time_to_answer=None, + average_time_in_draft=None, + average_time_in_labels=None, + num_issues_opened=2, + num_issues_closed=1, + num_mentor_count=0, + labels=None, + search_query="repo:test/repo is:issue", + hide_label_metrics=True, + hide_items_closed_count=False, + enable_mentor_count=False, + non_mentioning_links=False, + report_title="Test Issue Metrics", + output_file=output_file, + ghe="", + ) + + # Read and verify the markdown content + with open(output_file, 'r') as f: + content = f.read() + + # Check for assignee column header + self.assertIn("| Assignee |", content) + + # Check for assignee data - alice should be linked + self.assertIn("[alice](https://github.com/alice)", content) + + # Check for None assignee + self.assertIn("| None |", content) + + # Check that both assignee and author columns are present + self.assertIn("| Author |", content) + + finally: + os.unlink(output_file) + + def test_assignee_in_json_output(self): + """Test that assignee information appears correctly in JSON output.""" + issues_with_metrics = [ + IssueWithMetrics( + title="Test Issue 1", + html_url="https://github.com/test/repo/issues/1", + author="john", + assignee="alice", + assignees=["alice", "bob"], + time_to_first_response=timedelta(hours=2), + time_to_close=timedelta(days=1), + created_at=datetime.now() - timedelta(days=2), + ), + IssueWithMetrics( + title="Test Issue 2", + html_url="https://github.com/test/repo/issues/2", + author="jane", + assignee=None, + assignees=[], + time_to_first_response=timedelta(hours=4), + time_to_close=None, + created_at=datetime.now() - timedelta(days=1), + ), + ] + + with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: + output_file = f.name + + try: + json_output = write_to_json( + issues_with_metrics=issues_with_metrics, + stats_time_to_first_response={"avg": timedelta(hours=3), "med": timedelta(hours=3), "90p": timedelta(hours=4)}, + stats_time_to_close={"avg": timedelta(days=1), "med": timedelta(days=1), "90p": timedelta(days=1)}, + stats_time_to_answer=None, + stats_time_in_draft=None, + stats_time_in_labels=None, + num_issues_opened=2, + num_issues_closed=1, + num_mentor_count=0, + search_query="repo:test/repo is:issue", + output_file=output_file, + ) + + # Parse the JSON output + data = json.loads(json_output) + + # Check that assignee fields are present + issue1 = data["issues"][0] + self.assertEqual(issue1["assignee"], "alice") + self.assertEqual(issue1["assignees"], ["alice", "bob"]) + self.assertEqual(issue1["author"], "john") + + issue2 = data["issues"][1] + self.assertIsNone(issue2["assignee"]) + self.assertEqual(issue2["assignees"], []) + self.assertEqual(issue2["author"], "jane") + + finally: + os.unlink(output_file) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 3e25f02924f2ba2365b833f3d146775c8344c271 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 18:41:56 +0000 Subject: [PATCH 314/358] chore(deps): bump python from `d97b595` to `f2fdaec` Bumps python from `d97b595` to `f2fdaec`. --- updated-dependencies: - dependency-name: python dependency-version: 3.13-slim dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 22d76a14..f7112a5b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.13-slim@sha256:d97b595c5f4ac718102e5a5a91adaf04b22e852961a698411637c718d45867c8 +FROM python:3.13-slim@sha256:f2fdaec50160418e0c2867ba3e254755edd067171725886d5d303fd7057bbf81 LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From 8e1df89b649b7928317566a53a88940448aecfd2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 18:51:44 +0000 Subject: [PATCH 315/358] chore(deps): bump the dependencies group with 2 updates Bumps the dependencies group with 2 updates: [github/contributors](https://github.com/github/contributors) and [github/codeql-action](https://github.com/github/codeql-action). Updates `github/contributors` from 1.5.8 to 1.5.9 - [Release notes](https://github.com/github/contributors/releases) - [Commits](https://github.com/github/contributors/compare/6949781e2a2575cba21a80325c9dd6014f5c898b...4d90d92531d4c5775be5a70c119ca7c0be165964) Updates `github/codeql-action` from 3.28.19 to 3.29.0 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/fca7ace96b7d713c7035871441bd52efbe39e27e...ce28f5bb42b7a9f2c824e633a3f6ee835bab6858) --- updated-dependencies: - dependency-name: github/contributors dependency-version: 1.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: github/codeql-action dependency-version: 3.29.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/contributor_report.yaml | 2 +- .github/workflows/scorecard.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/contributor_report.yaml b/.github/workflows/contributor_report.yaml index 1385546e..d15c1841 100644 --- a/.github/workflows/contributor_report.yaml +++ b/.github/workflows/contributor_report.yaml @@ -27,7 +27,7 @@ jobs: echo "END_DATE=$end_date" >> "$GITHUB_ENV" - name: Run contributor action - uses: github/contributors@6949781e2a2575cba21a80325c9dd6014f5c898b # v1.5.8 + uses: github/contributors@4d90d92531d4c5775be5a70c119ca7c0be165964 # v1.5.9 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} START_DATE: ${{ env.START_DATE }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index cfebfa81..b1ce6afa 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.24.9 + uses: github/codeql-action/upload-sarif@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.24.9 with: sarif_file: results.sarif From 9b0856e8008e41c67f8bb4e2c7001e40a849a53c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 18:53:06 +0000 Subject: [PATCH 316/358] chore(deps): bump the dependencies group with 3 updates Bumps the dependencies group with 3 updates: [mypy](https://github.com/python/mypy), [pytest-cov](https://github.com/pytest-dev/pytest-cov) and [types-requests](https://github.com/typeshed-internal/stub_uploader). Updates `mypy` from 1.16.0 to 1.16.1 - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.16.0...v1.16.1) Updates `pytest-cov` from 6.1.1 to 6.2.1 - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v6.1.1...v6.2.1) Updates `types-requests` from 2.32.0.20250602 to 2.32.4.20250611 - [Commits](https://github.com/typeshed-internal/stub_uploader/commits) --- updated-dependencies: - dependency-name: mypy dependency-version: 1.16.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: pytest-cov dependency-version: 6.2.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: types-requests dependency-version: 2.32.4.20250611 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index 72949275..b69e8505 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,9 +1,9 @@ black==25.1.0 flake8==7.2.0 -mypy==1.16.0 +mypy==1.16.1 mypy-extensions==1.1.0 pylint==3.3.7 pytest==8.4.0 -pytest-cov==6.1.1 +pytest-cov==6.2.1 types-pytz==2025.2.0.20250516 -types-requests==2.32.0.20250602 +types-requests==2.32.4.20250611 From 3a626dd38d8881c6c06ba4d1b0a84eb4016fa1a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Jun 2025 22:22:11 +0000 Subject: [PATCH 317/358] Fix missing newlines at end of test files Co-authored-by: zkoppert <6935431+zkoppert@users.noreply.github.com> --- test_assignee_functionality.py | 2 +- test_assignee_integration.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test_assignee_functionality.py b/test_assignee_functionality.py index cbf2d14c..821235b9 100644 --- a/test_assignee_functionality.py +++ b/test_assignee_functionality.py @@ -93,4 +93,4 @@ def test_assignee_column_position(self): if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/test_assignee_integration.py b/test_assignee_integration.py index 8601484f..be79cc64 100644 --- a/test_assignee_integration.py +++ b/test_assignee_integration.py @@ -153,4 +153,4 @@ def test_assignee_in_json_output(self): if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() From d7d98d82b04442c4068f0ce9a9d5be1fbd1d406e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Jun 2025 22:32:06 +0000 Subject: [PATCH 318/358] Fix linting issues - remove unused imports, fix formatting, and encoding Co-authored-by: jmeridth <35014+jmeridth@users.noreply.github.com> --- markdown_writer.py | 4 +--- test_assignee_functionality.py | 8 +++++-- test_assignee_integration.py | 41 ++++++++++++++++++++++++---------- test_markdown_writer.py | 13 ++++++----- 4 files changed, 44 insertions(+), 22 deletions(-) diff --git a/markdown_writer.py b/markdown_writer.py index 7ff3a833..5e20b8c3 100644 --- a/markdown_writer.py +++ b/markdown_writer.py @@ -216,9 +216,7 @@ def write_to_markdown( else: file.write(" None |") if "Author" in columns: - file.write( - f" [{issue.author}](https://{endpoint}/{issue.author}) |" - ) + file.write(f" [{issue.author}](https://{endpoint}/{issue.author}) |") if "Time to first response" in columns: file.write(f" {issue.time_to_first_response} |") if "Time to close" in columns: diff --git a/test_assignee_functionality.py b/test_assignee_functionality.py index 821235b9..33f543e1 100644 --- a/test_assignee_functionality.py +++ b/test_assignee_functionality.py @@ -3,6 +3,7 @@ import os import unittest from unittest.mock import patch + from markdown_writer import get_non_hidden_columns @@ -88,8 +89,11 @@ def test_assignee_column_position(self): columns = get_non_hidden_columns(labels=None) assignee_index = columns.index("Assignee") author_index = columns.index("Author") - self.assertLess(assignee_index, author_index, - "Assignee column should appear before Author column") + self.assertLess( + assignee_index, + author_index, + "Assignee column should appear before Author column", + ) if __name__ == "__main__": diff --git a/test_assignee_integration.py b/test_assignee_integration.py index be79cc64..3495b778 100644 --- a/test_assignee_integration.py +++ b/test_assignee_integration.py @@ -4,11 +4,12 @@ import os import tempfile import unittest -from unittest.mock import MagicMock, patch from datetime import datetime, timedelta +from unittest.mock import patch + from classes import IssueWithMetrics -from markdown_writer import write_to_markdown from json_writer import write_to_json +from markdown_writer import write_to_markdown class TestAssigneeIntegration(unittest.TestCase): @@ -47,14 +48,22 @@ def test_assignee_in_markdown_output(self): ), ] - with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f: + with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f: output_file = f.name try: write_to_markdown( issues_with_metrics=issues_with_metrics, - average_time_to_first_response={"avg": timedelta(hours=3), "med": timedelta(hours=3), "90p": timedelta(hours=4)}, - average_time_to_close={"avg": timedelta(days=1), "med": timedelta(days=1), "90p": timedelta(days=1)}, + average_time_to_first_response={ + "avg": timedelta(hours=3), + "med": timedelta(hours=3), + "90p": timedelta(hours=4), + }, + average_time_to_close={ + "avg": timedelta(days=1), + "med": timedelta(days=1), + "90p": timedelta(days=1), + }, average_time_to_answer=None, average_time_in_draft=None, average_time_in_labels=None, @@ -73,18 +82,18 @@ def test_assignee_in_markdown_output(self): ) # Read and verify the markdown content - with open(output_file, 'r') as f: + with open(output_file, "r", encoding="utf-8") as f: content = f.read() # Check for assignee column header self.assertIn("| Assignee |", content) - + # Check for assignee data - alice should be linked self.assertIn("[alice](https://github.com/alice)", content) - + # Check for None assignee self.assertIn("| None |", content) - + # Check that both assignee and author columns are present self.assertIn("| Author |", content) @@ -116,14 +125,22 @@ def test_assignee_in_json_output(self): ), ] - with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: output_file = f.name try: json_output = write_to_json( issues_with_metrics=issues_with_metrics, - stats_time_to_first_response={"avg": timedelta(hours=3), "med": timedelta(hours=3), "90p": timedelta(hours=4)}, - stats_time_to_close={"avg": timedelta(days=1), "med": timedelta(days=1), "90p": timedelta(days=1)}, + stats_time_to_first_response={ + "avg": timedelta(hours=3), + "med": timedelta(hours=3), + "90p": timedelta(hours=4), + }, + stats_time_to_close={ + "avg": timedelta(days=1), + "med": timedelta(days=1), + "90p": timedelta(days=1), + }, stats_time_to_answer=None, stats_time_in_draft=None, stats_time_in_labels=None, diff --git a/test_markdown_writer.py b/test_markdown_writer.py index 4c3b9b2a..794177c8 100644 --- a/test_markdown_writer.py +++ b/test_markdown_writer.py @@ -137,8 +137,9 @@ def test_write_to_markdown(self): "| Title | URL | Assignee | Author | Time to first response | Time to close |" " Time to answer | Time in draft | Time spent in bug | Created At |\n" "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n" - "| Issue 1 | https://github.com/user/repo/issues/1 | [charlie](https://github.com/charlie) | [alice](https://github.com/alice) | 1 day, 0:00:00 | " - "2 days, 0:00:00 | 3 days, 0:00:00 | 1 day, 0:00:00 | 4 days, 0:00:00 | -5 days, 0:00:00 |\n" + "| Issue 1 | https://github.com/user/repo/issues/1 | [charlie](https://github.com/charlie) | " + "[alice](https://github.com/alice) | 1 day, 0:00:00 | 2 days, 0:00:00 | 3 days, 0:00:00 | " + "1 day, 0:00:00 | 4 days, 0:00:00 | -5 days, 0:00:00 |\n" "| Issue 2 | https://github.com/user/repo/issues/2 | None | [bob](https://github.com/bob) | 3 days, 0:00:00 | " "4 days, 0:00:00 | 5 days, 0:00:00 | 1 day, 0:00:00 | 2 days, 0:00:00 | -5 days, 0:00:00 |\n\n" "_This report was generated with the [Issue Metrics Action](https://github.com/github/issue-metrics)_\n" @@ -251,8 +252,9 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): "| Title | URL | Assignee | Author | Time to first response | Time to close |" " Time to answer | Time in draft | Time spent in bug | Created At |\n" "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n" - "| Issue 1 | https://github.com/user/repo/issues/1 | [charlie](https://github.com/charlie) | [alice](https://github.com/alice) | 1 day, 0:00:00 | " - "2 days, 0:00:00 | 3 days, 0:00:00 | 1 day, 0:00:00 | 1 day, 0:00:00 | -5 days, 0:00:00 |\n" + "| Issue 1 | https://github.com/user/repo/issues/1 | [charlie](https://github.com/charlie) | " + "[alice](https://github.com/alice) | 1 day, 0:00:00 | 2 days, 0:00:00 | 3 days, 0:00:00 | " + "1 day, 0:00:00 | 1 day, 0:00:00 | -5 days, 0:00:00 |\n" "| feat| Issue 2 | https://github.com/user/repo/issues/2 | None | [bob](https://github.com/bob) | 3 days, 0:00:00 | " "4 days, 0:00:00 | 5 days, 0:00:00 | None | 2 days, 0:00:00 | -5 days, 0:00:00 |\n\n" "_This report was generated with the [Issue Metrics Action](https://github.com/github/issue-metrics)_\n" @@ -399,7 +401,8 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): "| Total number of items created | 2 |\n\n" "| Title | URL | Assignee | Author | Created At |\n" "| --- | --- | --- | --- | --- |\n" - "| Issue 1 | https://www.ghe.com/user/repo/issues/1 | [charlie](https://ghe.com/charlie) | [alice](https://ghe.com/alice) | -5 days, 0:00:00 |\n" + "| Issue 1 | https://www.ghe.com/user/repo/issues/1 | [charlie](https://ghe.com/charlie) | " + "[alice](https://ghe.com/alice) | -5 days, 0:00:00 |\n" "| Issue 2 | https://www.ghe.com/user/repo/issues/2 | None | [bob](https://ghe.com/bob) | -5 days, 0:00:00 |\n\n" "_This report was generated with the [Issue Metrics Action](https://github.com/github/issue-metrics)_\n" "Search query used to find these items: `repo:user/repo is:issue`\n" From d84ddfb650615bbdcd21d3365f8319ea7a455ef6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Jun 2025 04:26:22 +0000 Subject: [PATCH 319/358] feat: display all assignees instead of just primary assignee in markdown output Co-authored-by: zkoppert <6935431+zkoppert@users.noreply.github.com> --- markdown_writer.py | 11 +++-- test_assignee_functionality.py | 83 ++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 5 deletions(-) diff --git a/markdown_writer.py b/markdown_writer.py index 5e20b8c3..efaf0ac6 100644 --- a/markdown_writer.py +++ b/markdown_writer.py @@ -208,11 +208,12 @@ def write_to_markdown( else: file.write(f"| {issue.title} | {issue.html_url} |") if "Assignee" in columns: - if issue.assignee: - file.write( - f" [{issue.assignee}](https://{endpoint}/" - f"{issue.assignee}) |" - ) + if issue.assignees: + assignee_links = [ + f"[{assignee}](https://{endpoint}/{assignee})" + for assignee in issue.assignees + ] + file.write(f" {', '.join(assignee_links)} |") else: file.write(" None |") if "Author" in columns: diff --git a/test_assignee_functionality.py b/test_assignee_functionality.py index 33f543e1..4513db23 100644 --- a/test_assignee_functionality.py +++ b/test_assignee_functionality.py @@ -95,6 +95,89 @@ def test_assignee_column_position(self): "Assignee column should appear before Author column", ) + def test_multiple_assignees_rendering_logic(self): + """Test that multiple assignees are rendered correctly in assignee column.""" + from classes import IssueWithMetrics + from io import StringIO + + # Test the assignee rendering logic directly + endpoint = "github.com" + columns = ["Title", "URL", "Assignee", "Author"] + + # Test case 1: Multiple assignees + issue_multiple = IssueWithMetrics( + title="Test Issue with Multiple Assignees", + html_url="https://github.com/test/repo/issues/1", + author="testuser", + assignee="alice", + assignees=["alice", "bob", "charlie"] + ) + + # Simulate the new rendering logic + if "Assignee" in columns: + if issue_multiple.assignees: + assignee_links = [ + f"[{assignee}](https://{endpoint}/{assignee})" + for assignee in issue_multiple.assignees + ] + multiple_output = f" {', '.join(assignee_links)} |" + else: + multiple_output = " None |" + + expected_multiple = " [alice](https://github.com/alice), [bob](https://github.com/bob), [charlie](https://github.com/charlie) |" + self.assertEqual(multiple_output, expected_multiple, + "Multiple assignees should be rendered as comma-separated links") + + # Test case 2: Single assignee + issue_single = IssueWithMetrics( + title="Test Issue with Single Assignee", + html_url="https://github.com/test/repo/issues/2", + author="testuser", + assignee="alice", + assignees=["alice"] + ) + + if "Assignee" in columns: + if issue_single.assignees: + assignee_links = [ + f"[{assignee}](https://{endpoint}/{assignee})" + for assignee in issue_single.assignees + ] + single_output = f" {', '.join(assignee_links)} |" + else: + single_output = " None |" + + expected_single = " [alice](https://github.com/alice) |" + self.assertEqual(single_output, expected_single, + "Single assignee should be rendered as a single link") + + # Test case 3: No assignees + issue_none = IssueWithMetrics( + title="Test Issue with No Assignees", + html_url="https://github.com/test/repo/issues/3", + author="testuser", + assignee=None, + assignees=[] + ) + + if "Assignee" in columns: + if issue_none.assignees: + assignee_links = [ + f"[{assignee}](https://{endpoint}/{assignee})" + for assignee in issue_none.assignees + ] + none_output = f" {', '.join(assignee_links)} |" + else: + none_output = " None |" + + expected_none = " None |" + self.assertEqual(none_output, expected_none, + "No assignees should be rendered as 'None'") + + print(f"✅ Multiple assignees test: {expected_multiple}") + print(f"✅ Single assignee test: {expected_single}") + print(f"✅ No assignees test: {expected_none}") + if __name__ == "__main__": unittest.main() From 3a29d679c8e0af1b116150106d53be36f8d2dc59 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Jun 2025 04:30:56 +0000 Subject: [PATCH 320/358] docs: add HIDE_ASSIGNEE configuration to README.md Co-authored-by: zkoppert <6935431+zkoppert@users.noreply.github.com> --- README.md | 1 + test_assignee_functionality.py | 54 +++++++++++++++++++--------------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index b08ecea6..e63018ca 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,7 @@ This action can be configured to authenticate with GitHub App Installation or Pe | ----------------------------- | -------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `GH_ENTERPRISE_URL` | False | `""` | URL of GitHub Enterprise instance to use for auth instead of github.com | | `RATE_LIMIT_BYPASS` | False | `false` | If set to `true`, the rate limit will be bypassed. This is useful if being run on an local GitHub server with rate limiting disabled. | +| `HIDE_ASSIGNEE` | False | False | If set to `true`, the assignee will not be displayed in the generated Markdown file. | | `HIDE_AUTHOR` | False | False | If set to `true`, the author will not be displayed in the generated Markdown file. | | `HIDE_ITEMS_CLOSED_COUNT` | False | False | If set to `true`, the number of items closed metric will not be displayed in the generated Markdown file. | | `HIDE_LABEL_METRICS` | False | False | If set to `true`, the time in label metrics will not be displayed in the generated Markdown file. | diff --git a/test_assignee_functionality.py b/test_assignee_functionality.py index 4513db23..a28b3851 100644 --- a/test_assignee_functionality.py +++ b/test_assignee_functionality.py @@ -97,23 +97,24 @@ def test_assignee_column_position(self): def test_multiple_assignees_rendering_logic(self): """Test that multiple assignees are rendered correctly in assignee column.""" - from classes import IssueWithMetrics from io import StringIO - + + from classes import IssueWithMetrics + # Test the assignee rendering logic directly endpoint = "github.com" columns = ["Title", "URL", "Assignee", "Author"] - + # Test case 1: Multiple assignees issue_multiple = IssueWithMetrics( title="Test Issue with Multiple Assignees", html_url="https://github.com/test/repo/issues/1", author="testuser", assignee="alice", - assignees=["alice", "bob", "charlie"] + assignees=["alice", "bob", "charlie"], ) - - # Simulate the new rendering logic + + # Simulate the new rendering logic if "Assignee" in columns: if issue_multiple.assignees: assignee_links = [ @@ -123,20 +124,23 @@ def test_multiple_assignees_rendering_logic(self): multiple_output = f" {', '.join(assignee_links)} |" else: multiple_output = " None |" - + expected_multiple = " [alice](https://github.com/alice), [bob](https://github.com/bob), [charlie](https://github.com/charlie) |" - self.assertEqual(multiple_output, expected_multiple, - "Multiple assignees should be rendered as comma-separated links") - + self.assertEqual( + multiple_output, + expected_multiple, + "Multiple assignees should be rendered as comma-separated links", + ) + # Test case 2: Single assignee issue_single = IssueWithMetrics( title="Test Issue with Single Assignee", html_url="https://github.com/test/repo/issues/2", author="testuser", assignee="alice", - assignees=["alice"] + assignees=["alice"], ) - + if "Assignee" in columns: if issue_single.assignees: assignee_links = [ @@ -146,20 +150,23 @@ def test_multiple_assignees_rendering_logic(self): single_output = f" {', '.join(assignee_links)} |" else: single_output = " None |" - + expected_single = " [alice](https://github.com/alice) |" - self.assertEqual(single_output, expected_single, - "Single assignee should be rendered as a single link") - + self.assertEqual( + single_output, + expected_single, + "Single assignee should be rendered as a single link", + ) + # Test case 3: No assignees issue_none = IssueWithMetrics( title="Test Issue with No Assignees", html_url="https://github.com/test/repo/issues/3", - author="testuser", + author="testuser", assignee=None, - assignees=[] + assignees=[], ) - + if "Assignee" in columns: if issue_none.assignees: assignee_links = [ @@ -169,11 +176,12 @@ def test_multiple_assignees_rendering_logic(self): none_output = f" {', '.join(assignee_links)} |" else: none_output = " None |" - + expected_none = " None |" - self.assertEqual(none_output, expected_none, - "No assignees should be rendered as 'None'") - + self.assertEqual( + none_output, expected_none, "No assignees should be rendered as 'None'" + ) + print(f"✅ Multiple assignees test: {expected_multiple}") print(f"✅ Single assignee test: {expected_single}") print(f"✅ No assignees test: {expected_none}") From 9aa2fd3003a272608dcc253a964a9b9522b7d40e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Jun 2025 16:24:12 +0000 Subject: [PATCH 321/358] fix: resolve linting issues in test files Co-authored-by: jmeridth <35014+jmeridth@users.noreply.github.com> --- test_assignee_functionality.py | 14 ++++++++++---- test_markdown_writer.py | 3 ++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/test_assignee_functionality.py b/test_assignee_functionality.py index a28b3851..1c12a9b1 100644 --- a/test_assignee_functionality.py +++ b/test_assignee_functionality.py @@ -4,6 +4,7 @@ import unittest from unittest.mock import patch +from classes import IssueWithMetrics from markdown_writer import get_non_hidden_columns @@ -97,14 +98,16 @@ def test_assignee_column_position(self): def test_multiple_assignees_rendering_logic(self): """Test that multiple assignees are rendered correctly in assignee column.""" - from io import StringIO - - from classes import IssueWithMetrics # Test the assignee rendering logic directly endpoint = "github.com" columns = ["Title", "URL", "Assignee", "Author"] + # Initialize variables + multiple_output = "" + single_output = "" + none_output = "" + # Test case 1: Multiple assignees issue_multiple = IssueWithMetrics( title="Test Issue with Multiple Assignees", @@ -125,7 +128,10 @@ def test_multiple_assignees_rendering_logic(self): else: multiple_output = " None |" - expected_multiple = " [alice](https://github.com/alice), [bob](https://github.com/bob), [charlie](https://github.com/charlie) |" + expected_multiple = ( + " [alice](https://github.com/alice), [bob](https://github.com/bob), " + "[charlie](https://github.com/charlie) |" + ) self.assertEqual( multiple_output, expected_multiple, diff --git a/test_markdown_writer.py b/test_markdown_writer.py index 794177c8..bf3612cb 100644 --- a/test_markdown_writer.py +++ b/test_markdown_writer.py @@ -255,7 +255,8 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): "| Issue 1 | https://github.com/user/repo/issues/1 | [charlie](https://github.com/charlie) | " "[alice](https://github.com/alice) | 1 day, 0:00:00 | 2 days, 0:00:00 | 3 days, 0:00:00 | " "1 day, 0:00:00 | 1 day, 0:00:00 | -5 days, 0:00:00 |\n" - "| feat| Issue 2 | https://github.com/user/repo/issues/2 | None | [bob](https://github.com/bob) | 3 days, 0:00:00 | " + "| feat| Issue 2 | https://github.com/user/repo/issues/2 | None | " + "[bob](https://github.com/bob) | 3 days, 0:00:00 | " "4 days, 0:00:00 | 5 days, 0:00:00 | None | 2 days, 0:00:00 | -5 days, 0:00:00 |\n\n" "_This report was generated with the [Issue Metrics Action](https://github.com/github/issue-metrics)_\n" ) From d87d4e6673c51c210e077e446fe4c0e9e259bb7d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 19:07:38 +0000 Subject: [PATCH 322/358] chore(deps): bump the dependencies group with 2 updates Bumps the dependencies group with 2 updates: [flake8](https://github.com/pycqa/flake8) and [pytest](https://github.com/pytest-dev/pytest). Updates `flake8` from 7.2.0 to 7.3.0 - [Commits](https://github.com/pycqa/flake8/compare/7.2.0...7.3.0) Updates `pytest` from 8.4.0 to 8.4.1 - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.4.0...8.4.1) --- updated-dependencies: - dependency-name: flake8 dependency-version: 7.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: pytest dependency-version: 8.4.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index b69e8505..3b294cb5 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,9 +1,9 @@ black==25.1.0 -flake8==7.2.0 +flake8==7.3.0 mypy==1.16.1 mypy-extensions==1.1.0 pylint==3.3.7 -pytest==8.4.0 +pytest==8.4.1 pytest-cov==6.2.1 types-pytz==2025.2.0.20250516 types-requests==2.32.4.20250611 From 06a4304ebc5f4b5ca08e6af2cd533257451a1dcb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 20:38:30 +0000 Subject: [PATCH 323/358] chore(deps): bump python-dotenv in the dependencies group --- updated-dependencies: - dependency-name: python-dotenv dependency-version: 1.1.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index efe5a96e..2d48a848 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ github3.py==4.0.1 numpy==2.2.4 -python-dotenv==1.1.0 +python-dotenv==1.1.1 pytz==2025.2 requests==2.32.4 From 4dad97e7abb2bbcc0dd66359f3d76c85a393bcc4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 20:43:41 +0000 Subject: [PATCH 324/358] chore(deps): bump github/codeql-action in the dependencies group Bumps the dependencies group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 3.29.0 to 3.29.2 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/ce28f5bb42b7a9f2c824e633a3f6ee835bab6858...181d5eefc20863364f96762470ba6f862bdef56b) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.29.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index b1ce6afa..047161af 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.24.9 + uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.24.9 with: sarif_file: results.sarif From cd8e18d66f5a327ccef171f4633825be8ea1c602 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 19:22:21 +0000 Subject: [PATCH 325/358] chore(deps): bump python from `f2fdaec` to `6544e0e` Bumps python from `f2fdaec` to `6544e0e`. --- updated-dependencies: - dependency-name: python dependency-version: 3.13-slim dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f7112a5b..26a58174 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.13-slim@sha256:f2fdaec50160418e0c2867ba3e254755edd067171725886d5d303fd7057bbf81 +FROM python:3.13-slim@sha256:6544e0e002b40ae0f59bc3618b07c1e48064c4faed3a15ae2fbd2e8f663e8283 LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From fb669c9330d041c418139860d16458494f378b42 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Thu, 17 Jul 2025 23:20:40 +0000 Subject: [PATCH 326/358] feat: add status column feature with configs to hide it Signed-off-by: Zack Koppert --- README.md | 5 +++-- config.py | 6 ++++++ markdown_writer.py | 8 ++++++++ test_assignee_functionality.py | 28 ++++++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e63018ca..12e498b5 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,7 @@ This action can be configured to authenticate with GitHub App Installation or Pe ##### GitHub App Installation | field | required | default | description | -| ---------------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ---------------------------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `GH_APP_ID` | True | `""` | GitHub Application ID. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. | | `GH_APP_INSTALLATION_ID` | True | `""` | GitHub Application Installation ID. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. | | `GH_APP_PRIVATE_KEY` | True | `""` | GitHub Application Private Key. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. | @@ -154,7 +154,8 @@ This action can be configured to authenticate with GitHub App Installation or Pe | `HIDE_TIME_TO_ANSWER` | False | False | If set to `true`, the time to answer a discussion will not be displayed in the generated Markdown file. | | `HIDE_TIME_TO_CLOSE` | False | False | If set to `true`, the time to close will not be displayed in the generated Markdown file. | | `HIDE_TIME_TO_FIRST_RESPONSE` | False | False | If set to `true`, the time to first response will not be displayed in the generated Markdown file. | -| `HIDE_CREATED_AT` | False | True | If set to `true`, the creation timestmap will not be displayed in the generated Markdown file. | +| `HIDE_STATUS` | False | True | If set to `true`, the status column will not be shown | +| `HIDE_CREATED_AT` | False | True | If set to `true`, the creation timestamp will not be displayed in the generated Markdown file. | | `DRAFT_PR_TRACKING` | False | False | If set to `true`, draft PRs will be included in the metrics as a new column and in the summary stats. | | `IGNORE_USERS` | False | False | A comma separated list of users to ignore when calculating metrics. (ie. `IGNORE_USERS: 'user1,user2'`). To ignore bots, append `[bot]` to the user (ie. `IGNORE_USERS: 'github-actions[bot]'`) Users in this list will also have their authored issues and pull requests removed from the Markdown table. | | `ENABLE_MENTOR_COUNT` | False | False | If set to 'TRUE' count number of comments users left on discussions, issues and PRs and display number of active mentors | diff --git a/config.py b/config.py index 55768dce..baa0097d 100644 --- a/config.py +++ b/config.py @@ -39,6 +39,8 @@ class EnvVars: hide_time_to_close (bool): If true, the time to close metric is hidden in the output hide_time_to_first_response (bool): If true, the time to first response metric is hidden in the output + hide_created_at (bool): If true, the created at timestamp is hidden in the output + hide_status (bool): If true, the status column is hidden in the output ignore_users (List[str]): List of usernames to ignore when calculating metrics labels_to_measure (List[str]): List of labels to measure how much time the label is applied enable_mentor_count (bool): If set to TRUE, compute number of mentors @@ -73,6 +75,7 @@ def __init__( hide_time_to_close: bool, hide_time_to_first_response: bool, hide_created_at: bool, + hide_status: bool, # New attribute ignore_user: List[str], labels_to_measure: List[str], enable_mentor_count: bool, @@ -102,6 +105,7 @@ def __init__( self.hide_time_to_close = hide_time_to_close self.hide_time_to_first_response = hide_time_to_first_response self.hide_created_at = hide_created_at + self.hide_status = hide_status # Initialize the new attribute self.enable_mentor_count = enable_mentor_count self.min_mentor_comments = min_mentor_comments self.max_comments_eval = max_comments_eval @@ -238,6 +242,7 @@ def get_env_vars(test: bool = False) -> EnvVars: hide_time_to_close = get_bool_env_var("HIDE_TIME_TO_CLOSE", False) hide_time_to_first_response = get_bool_env_var("HIDE_TIME_TO_FIRST_RESPONSE", False) hide_created_at = get_bool_env_var("HIDE_CREATED_AT", True) + hide_status = get_bool_env_var("HIDE_STATUS", True) # New attribute enable_mentor_count = get_bool_env_var("ENABLE_MENTOR_COUNT", False) min_mentor_comments = os.getenv("MIN_MENTOR_COMMENTS", "10") max_comments_eval = os.getenv("MAX_COMMENTS_EVAL", "20") @@ -259,6 +264,7 @@ def get_env_vars(test: bool = False) -> EnvVars: hide_time_to_close, hide_time_to_first_response, hide_created_at, + hide_status, ignore_users_list, labels_to_measure_list, enable_mentor_count, diff --git a/markdown_writer.py b/markdown_writer.py index efaf0ac6..e83fa23d 100644 --- a/markdown_writer.py +++ b/markdown_writer.py @@ -75,6 +75,10 @@ def get_non_hidden_columns(labels) -> List[str]: if not hide_time_to_answer: columns.append("Time to answer") + hide_status = env_vars.hide_status # Check the new attribute + if not hide_status: + columns.append("Status") # Add the 'status' column + enable_time_in_draft = env_vars.draft_pr_tracking if enable_time_in_draft: columns.append("Time in draft") @@ -232,6 +236,8 @@ def write_to_markdown( file.write(f" {issue.label_metrics[label]} |") if "Created At" in columns: file.write(f" {issue.created_at} |") + if "Status" in columns: # Check if 'Status' column is to be displayed + file.write(f" {issue.status} |") # Write the status of the issue file.write("\n") file.write( "\n_This report was generated with the \ @@ -324,6 +330,8 @@ def write_overall_metrics_tables( f"| {stats_time_in_labels['med'][label]} " f"| {stats_time_in_labels['90p'][label]} |\n" ) + if "Status" in columns: # Add logic for the 'status' column + file.write("| Status | | | |\n") file.write("\n") # Write count stats to a separate table diff --git a/test_assignee_functionality.py b/test_assignee_functionality.py index 1c12a9b1..890fc62c 100644 --- a/test_assignee_functionality.py +++ b/test_assignee_functionality.py @@ -75,6 +75,34 @@ def test_get_non_hidden_columns_hides_both_assignee_and_author(self): self.assertNotIn("Assignee", columns) self.assertNotIn("Author", columns) + @patch.dict( + os.environ, + { + "GH_TOKEN": "test_token", + "SEARCH_QUERY": "is:issue is:open repo:user/repo", + "HIDE_STATUS": "false", + }, + clear=True, + ) + def test_get_non_hidden_columns_includes_status_by_default(self): + """Test that status column is included by default.""" + columns = get_non_hidden_columns(labels=None) + self.assertIn("Status", columns) + + @patch.dict( + os.environ, + { + "GH_TOKEN": "test_token", + "SEARCH_QUERY": "is:issue is:open repo:user/repo", + "HIDE_STATUS": "true", + }, + clear=True, + ) + def test_get_non_hidden_columns_hides_status_when_env_set(self): + """Test that status column is hidden when HIDE_STATUS is true.""" + columns = get_non_hidden_columns(labels=None) + self.assertNotIn("Status", columns) + def test_assignee_column_position(self): """Test that assignee column appears before author column.""" with patch.dict( From 155da571f82b5998414aae45ba10d8b603931111 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Thu, 17 Jul 2025 16:28:31 -0700 Subject: [PATCH 327/358] fix: add hide_status to the env class and fix tests Signed-off-by: Zack Koppert --- config.py | 7 ++++--- test_config.py | 4 ++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/config.py b/config.py index baa0097d..475aa34d 100644 --- a/config.py +++ b/config.py @@ -75,7 +75,7 @@ def __init__( hide_time_to_close: bool, hide_time_to_first_response: bool, hide_created_at: bool, - hide_status: bool, # New attribute + hide_status: bool, ignore_user: List[str], labels_to_measure: List[str], enable_mentor_count: bool, @@ -105,7 +105,7 @@ def __init__( self.hide_time_to_close = hide_time_to_close self.hide_time_to_first_response = hide_time_to_first_response self.hide_created_at = hide_created_at - self.hide_status = hide_status # Initialize the new attribute + self.hide_status = hide_status self.enable_mentor_count = enable_mentor_count self.min_mentor_comments = min_mentor_comments self.max_comments_eval = max_comments_eval @@ -134,6 +134,7 @@ def __repr__(self): f"{self.hide_time_to_close}," f"{self.hide_time_to_first_response}," f"{self.hide_created_at}," + f"{self.hide_status}," f"{self.ignore_users}," f"{self.labels_to_measure}," f"{self.enable_mentor_count}," @@ -242,7 +243,7 @@ def get_env_vars(test: bool = False) -> EnvVars: hide_time_to_close = get_bool_env_var("HIDE_TIME_TO_CLOSE", False) hide_time_to_first_response = get_bool_env_var("HIDE_TIME_TO_FIRST_RESPONSE", False) hide_created_at = get_bool_env_var("HIDE_CREATED_AT", True) - hide_status = get_bool_env_var("HIDE_STATUS", True) # New attribute + hide_status = get_bool_env_var("HIDE_STATUS", True) enable_mentor_count = get_bool_env_var("ENABLE_MENTOR_COUNT", False) min_mentor_comments = os.getenv("MIN_MENTOR_COMMENTS", "10") max_comments_eval = os.getenv("MAX_COMMENTS_EVAL", "20") diff --git a/test_config.py b/test_config.py index 537d1573..49435fa6 100644 --- a/test_config.py +++ b/test_config.py @@ -131,6 +131,7 @@ def test_get_env_vars_with_github_app(self): hide_time_to_close=False, hide_time_to_first_response=False, hide_created_at=True, + hide_status=True, ignore_user=[], labels_to_measure=[], enable_mentor_count=False, @@ -186,6 +187,7 @@ def test_get_env_vars_with_token(self): hide_time_to_close=False, hide_time_to_first_response=False, hide_created_at=True, + hide_status=True, ignore_user=[], labels_to_measure=[], enable_mentor_count=False, @@ -276,6 +278,7 @@ def test_get_env_vars_optional_values(self): hide_time_to_close=True, hide_time_to_first_response=True, hide_created_at=True, + hide_status=True, ignore_user=[], labels_to_measure=["waiting-for-review", "waiting-for-manager"], enable_mentor_count=False, @@ -320,6 +323,7 @@ def test_get_env_vars_optionals_are_defaulted(self): hide_time_to_close=False, hide_time_to_first_response=False, hide_created_at=True, + hide_status=True, ignore_user=[], labels_to_measure=[], enable_mentor_count=False, From 38ca2915327c8ae6a9cc2a1e06d7acd883df950c Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Thu, 17 Jul 2025 17:02:33 -0700 Subject: [PATCH 328/358] feat: connect status up to actual api call Signed-off-by: Zack Koppert --- classes.py | 3 +++ issue_metrics.py | 4 ++++ markdown_writer.py | 8 ++++---- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/classes.py b/classes.py index 20ab9b37..d24f4301 100644 --- a/classes.py +++ b/classes.py @@ -24,6 +24,7 @@ class IssueWithMetrics: label_metrics (dict, optional): A dictionary containing the label metrics mentor_activity (dict, optional): A dictionary containing active mentors created_at (datetime, optional): The time the issue was created. + status (str, optional): The status of the issue, e.g., "open", "closed as completed", """ # pylint: disable=too-many-instance-attributes @@ -42,6 +43,7 @@ def __init__( created_at=None, assignee=None, assignees=None, + status=None, ): self.title = title self.html_url = html_url @@ -55,3 +57,4 @@ def __init__( self.label_metrics = labels_metrics self.mentor_activity = mentor_activity self.created_at = created_at + self.status = status diff --git a/issue_metrics.py b/issue_metrics.py index e7f59820..6664535b 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -175,8 +175,12 @@ def get_per_issue_metrics( issue_with_metrics.time_to_close = measure_time_to_close( issue, None ) + if env_vars.hide_status is False: + issue_with_metrics.status = f"{issue.issue.state} as {issue.issue.state_reason}" # type: ignore elif issue.state == "open": # type: ignore num_issues_open += 1 + if env_vars.hide_status is False: + issue_with_metrics.status = f"{issue.issue.state}" # type: ignore if not env_vars.hide_created_at: if isinstance(issue, github3.search.IssueSearchResult): # type: ignore issue_with_metrics.created_at = issue.issue.created_at # type: ignore diff --git a/markdown_writer.py b/markdown_writer.py index e83fa23d..7a88a8ac 100644 --- a/markdown_writer.py +++ b/markdown_writer.py @@ -75,9 +75,9 @@ def get_non_hidden_columns(labels) -> List[str]: if not hide_time_to_answer: columns.append("Time to answer") - hide_status = env_vars.hide_status # Check the new attribute + hide_status = env_vars.hide_status if not hide_status: - columns.append("Status") # Add the 'status' column + columns.append("Status") enable_time_in_draft = env_vars.draft_pr_tracking if enable_time_in_draft: @@ -236,8 +236,8 @@ def write_to_markdown( file.write(f" {issue.label_metrics[label]} |") if "Created At" in columns: file.write(f" {issue.created_at} |") - if "Status" in columns: # Check if 'Status' column is to be displayed - file.write(f" {issue.status} |") # Write the status of the issue + if "Status" in columns: + file.write(f" {issue.status} |") file.write("\n") file.write( "\n_This report was generated with the \ From f6ab3d4ff6eb802a7e66ddbfe62040594377ccb5 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Thu, 17 Jul 2025 18:33:58 -0700 Subject: [PATCH 329/358] test: fix tests and add test for hide status is true Signed-off-by: Zack Koppert --- test_markdown_writer.py | 140 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 129 insertions(+), 11 deletions(-) diff --git a/test_markdown_writer.py b/test_markdown_writer.py index bf3612cb..0fbcad37 100644 --- a/test_markdown_writer.py +++ b/test_markdown_writer.py @@ -23,6 +23,7 @@ "GH_TOKEN": "test_token", "DRAFT_PR_TRACKING": "True", "HIDE_CREATED_AT": "False", + "HIDE_STATUS": "False", }, ) class TestWriteToMarkdown(unittest.TestCase): @@ -128,20 +129,21 @@ def test_write_to_markdown(self): "| Time to answer | 4 days, 0:00:00 | 4 days, 0:00:00 | 4 days, 0:00:00 |\n" "| Time in draft | 1 day, 0:00:00 | 1 day, 0:00:00 | 1 day, 0:00:00 |\n" "| Time spent in bug | 1 day, 12:00:00 | 1 day, 12:00:00 | 1 day, 12:00:00 |\n" + "| Status | | | |\n" "\n" "| Metric | Count |\n" "| --- | ---: |\n" "| Number of items that remain open | 2 |\n" "| Number of items closed | 1 |\n" "| Total number of items created | 2 |\n\n" - "| Title | URL | Assignee | Author | Time to first response | Time to close |" - " Time to answer | Time in draft | Time spent in bug | Created At |\n" - "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n" + "| Title | URL | Assignee | Author | Time to first response | Time to close | " + "Time to answer | Status | Time in draft | Time spent in bug | Created At |\n" + "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n" "| Issue 1 | https://github.com/user/repo/issues/1 | [charlie](https://github.com/charlie) | " "[alice](https://github.com/alice) | 1 day, 0:00:00 | 2 days, 0:00:00 | 3 days, 0:00:00 | " - "1 day, 0:00:00 | 4 days, 0:00:00 | -5 days, 0:00:00 |\n" + "1 day, 0:00:00 | 4 days, 0:00:00 | -5 days, 0:00:00 | None |\n" "| Issue 2 | https://github.com/user/repo/issues/2 | None | [bob](https://github.com/bob) | 3 days, 0:00:00 | " - "4 days, 0:00:00 | 5 days, 0:00:00 | 1 day, 0:00:00 | 2 days, 0:00:00 | -5 days, 0:00:00 |\n\n" + "4 days, 0:00:00 | 5 days, 0:00:00 | 1 day, 0:00:00 | 2 days, 0:00:00 | -5 days, 0:00:00 | None |\n\n" "_This report was generated with the [Issue Metrics Action](https://github.com/github/issue-metrics)_\n" "Search query used to find these items: `is:issue is:open label:bug`\n" ) @@ -182,6 +184,7 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): time_to_first_response=timedelta(days=3), time_to_close=timedelta(days=4), time_to_answer=timedelta(days=5), + time_in_draft=None, labels_metrics={"bug": timedelta(days=2)}, ), ] @@ -243,21 +246,22 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): "| Time to answer | 4 days, 0:00:00 | 4 days, 0:00:00 | 4 days, 0:00:00 |\n" "| Time in draft | 1 day, 0:00:00 | 1 day, 0:00:00 | 1 day, 0:00:00 |\n" "| Time spent in bug | 1 day, 12:00:00 | 1 day, 12:00:00 | 1 day, 12:00:00 |\n" + "| Status | | | |\n" "\n" "| Metric | Count |\n" "| --- | ---: |\n" "| Number of items that remain open | 2 |\n" "| Number of items closed | 1 |\n" "| Total number of items created | 2 |\n\n" - "| Title | URL | Assignee | Author | Time to first response | Time to close |" - " Time to answer | Time in draft | Time spent in bug | Created At |\n" - "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n" + "| Title | URL | Assignee | Author | Time to first response | Time to close | " + "Time to answer | Status | Time in draft | Time spent in bug | Created At |\n" + "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n" "| Issue 1 | https://github.com/user/repo/issues/1 | [charlie](https://github.com/charlie) | " "[alice](https://github.com/alice) | 1 day, 0:00:00 | 2 days, 0:00:00 | 3 days, 0:00:00 | " - "1 day, 0:00:00 | 1 day, 0:00:00 | -5 days, 0:00:00 |\n" + "1 day, 0:00:00 | 1 day, 0:00:00 | -5 days, 0:00:00 | None |\n" "| feat| Issue 2 | https://github.com/user/repo/issues/2 | None | " "[bob](https://github.com/bob) | 3 days, 0:00:00 | " - "4 days, 0:00:00 | 5 days, 0:00:00 | None | 2 days, 0:00:00 | -5 days, 0:00:00 |\n\n" + "4 days, 0:00:00 | 5 days, 0:00:00 | None | 2 days, 0:00:00 | -5 days, 0:00:00 | None |\n\n" "_This report was generated with the [Issue Metrics Action](https://github.com/github/issue-metrics)_\n" ) self.assertEqual(content, expected_content) @@ -308,6 +312,7 @@ def test_write_to_markdown_no_issues(self): "HIDE_LABEL_METRICS": "True", "NON_MENTIONING_LINKS": "True", "GH_ENTERPRISE_URL": "https://ghe.com", + "HIDE_STATUS": "False", }, ) class TestWriteToMarkdownWithEnv(unittest.TestCase): @@ -400,7 +405,116 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): "| Number of items that remain open | 2 |\n" "| Number of most active mentors | 5 |\n" "| Total number of items created | 2 |\n\n" - "| Title | URL | Assignee | Author | Created At |\n" + "| Title | URL | Assignee | Author | Status | Created At |\n" + "| --- | --- | --- | --- | --- | --- |\n" + "| Issue 1 | https://www.ghe.com/user/repo/issues/1 | [charlie](https://ghe.com/charlie) | " + "[alice](https://ghe.com/alice) | -5 days, 0:00:00 | None |\n" + "| Issue 2 | https://www.ghe.com/user/repo/issues/2 | None | [bob](https://ghe.com/bob) | -5 days, 0:00:00 | None |\n\n" + "_This report was generated with the [Issue Metrics Action](https://github.com/github/issue-metrics)_\n" + "Search query used to find these items: `repo:user/repo is:issue`\n" + ) + self.assertEqual(content, expected_content) + os.remove("issue_metrics.md") + + @patch.dict( + os.environ, + { + "SEARCH_QUERY": "is:open repo:user/repo", + "GH_TOKEN": "test_token", + "HIDE_CREATED_AT": "False", + "HIDE_TIME_TO_FIRST_RESPONSE": "True", + "HIDE_TIME_TO_CLOSE": "True", + "HIDE_TIME_TO_ANSWER": "True", + "HIDE_LABEL_METRICS": "True", + "NON_MENTIONING_LINKS": "True", + "GH_ENTERPRISE_URL": "https://ghe.com", + "HIDE_STATUS": "True", # Status column should be hidden + }, + ) + def test_writes_markdown_file_with_hidden_status_column(self): + """ + Test that write_to_markdown writes the correct markdown file + when HIDE_STATUS is set to True, ensuring the Status column + is not present in the output. + """ + # Create mock data + issues_with_metrics = [ + IssueWithMetrics( + title="Issue 1", + html_url="https://ghe.com/user/repo/issues/1", + author="alice", + assignee="charlie", + assignees=["charlie"], + created_at=timedelta(days=-5), + time_to_first_response=timedelta(minutes=10), + time_to_close=timedelta(days=1), + time_to_answer=timedelta(hours=2), + time_in_draft=timedelta(days=1), + labels_metrics={ + "label1": timedelta(days=1), + }, + ), + IssueWithMetrics( + title="Issue 2", + html_url="https://ghe.com/user/repo/issues/2", + author="bob", + assignee=None, + assignees=[], + created_at=timedelta(days=-5), + time_to_first_response=timedelta(minutes=20), + time_to_close=timedelta(days=2), + time_to_answer=timedelta(hours=4), + labels_metrics={ + "label1": timedelta(days=1), + }, + ), + ] + average_time_to_first_response = timedelta(minutes=15) + average_time_to_close = timedelta(days=1.5) + average_time_to_answer = timedelta(hours=3) + average_time_in_draft = timedelta(days=1) + average_time_in_labels = { + "label1": timedelta(days=1), + } + num_issues_opened = 2 + num_issues_closed = 2 + num_mentor_count = 5 + ghe = "https://ghe.com" + + # Call the function + write_to_markdown( + issues_with_metrics=issues_with_metrics, + average_time_to_first_response=average_time_to_first_response, + average_time_to_close=average_time_to_close, + average_time_to_answer=average_time_to_answer, + average_time_in_labels=average_time_in_labels, + average_time_in_draft=average_time_in_draft, + num_issues_opened=num_issues_opened, + num_issues_closed=num_issues_closed, + num_mentor_count=num_mentor_count, + labels=["label1"], + search_query="repo:user/repo is:issue", + hide_label_metrics=True, + hide_items_closed_count=True, + enable_mentor_count=True, + non_mentioning_links=True, + report_title="Issue Metrics", + output_file="issue_metrics.md", + ghe=ghe, + ) + + # Check that the function writes the correct markdown file + with open("issue_metrics.md", "r", encoding="utf-8") as file: + content = file.read() + + expected_content = ( + "# Issue Metrics\n\n" + "| Metric | Count |\n" + "| --- | ---: |\n" + "| Number of items that remain open | 2 |\n" + "| Number of most active mentors | 5 |\n" + "| Total number of items created | 2 |\n\n" + "| Title | URL | Assignee | Author | Created At |\n" # Status column should be missing "| --- | --- | --- | --- | --- |\n" "| Issue 1 | https://www.ghe.com/user/repo/issues/1 | [charlie](https://ghe.com/charlie) | " "[alice](https://ghe.com/alice) | -5 days, 0:00:00 |\n" @@ -410,3 +524,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): ) self.assertEqual(content, expected_content) os.remove("issue_metrics.md") + + +if __name__ == "__main__": + unittest.main() From 354444a7d5846ba085332bb3e1f94a6ec49c05c0 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Thu, 17 Jul 2025 18:41:11 -0700 Subject: [PATCH 330/358] docs: fix formatting Signed-off-by: Zack Koppert --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 12e498b5..596bfc38 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,7 @@ This action can be configured to authenticate with GitHub App Installation or Pe ##### GitHub App Installation | field | required | default | description | -| ---------------------------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ---------------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `GH_APP_ID` | True | `""` | GitHub Application ID. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. | | `GH_APP_INSTALLATION_ID` | True | `""` | GitHub Application Installation ID. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. | | `GH_APP_PRIVATE_KEY` | True | `""` | GitHub Application Private Key. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. | @@ -154,7 +154,7 @@ This action can be configured to authenticate with GitHub App Installation or Pe | `HIDE_TIME_TO_ANSWER` | False | False | If set to `true`, the time to answer a discussion will not be displayed in the generated Markdown file. | | `HIDE_TIME_TO_CLOSE` | False | False | If set to `true`, the time to close will not be displayed in the generated Markdown file. | | `HIDE_TIME_TO_FIRST_RESPONSE` | False | False | If set to `true`, the time to first response will not be displayed in the generated Markdown file. | -| `HIDE_STATUS` | False | True | If set to `true`, the status column will not be shown | +| `HIDE_STATUS` | False | True | If set to `true`, the status column will not be shown | | `HIDE_CREATED_AT` | False | True | If set to `true`, the creation timestamp will not be displayed in the generated Markdown file. | | `DRAFT_PR_TRACKING` | False | False | If set to `true`, draft PRs will be included in the metrics as a new column and in the summary stats. | | `IGNORE_USERS` | False | False | A comma separated list of users to ignore when calculating metrics. (ie. `IGNORE_USERS: 'user1,user2'`). To ignore bots, append `[bot]` to the user (ie. `IGNORE_USERS: 'github-actions[bot]'`) Users in this list will also have their authored issues and pull requests removed from the Markdown table. | From 117b899213b701849ea2ad27032d93ba08d59dad Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Thu, 17 Jul 2025 21:00:16 -0700 Subject: [PATCH 331/358] fix: pythonic logic Co-authored-by: Jason Meridth <35014+jmeridth@users.noreply.github.com> Signed-off-by: Zack Koppert --- issue_metrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/issue_metrics.py b/issue_metrics.py index 6664535b..53df4fa3 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -175,7 +175,7 @@ def get_per_issue_metrics( issue_with_metrics.time_to_close = measure_time_to_close( issue, None ) - if env_vars.hide_status is False: + if not env_vars.hide_status: issue_with_metrics.status = f"{issue.issue.state} as {issue.issue.state_reason}" # type: ignore elif issue.state == "open": # type: ignore num_issues_open += 1 From 543e100b8d0ab9112849b4a09dfe4b0a0a5badfc Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Thu, 17 Jul 2025 21:01:46 -0700 Subject: [PATCH 332/358] fix: pythonic logic Co-authored-by: Jason Meridth <35014+jmeridth@users.noreply.github.com> Signed-off-by: Zack Koppert --- issue_metrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/issue_metrics.py b/issue_metrics.py index 53df4fa3..0c86912c 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -179,7 +179,7 @@ def get_per_issue_metrics( issue_with_metrics.status = f"{issue.issue.state} as {issue.issue.state_reason}" # type: ignore elif issue.state == "open": # type: ignore num_issues_open += 1 - if env_vars.hide_status is False: + if not env_vars.hide_status: issue_with_metrics.status = f"{issue.issue.state}" # type: ignore if not env_vars.hide_created_at: if isinstance(issue, github3.search.IssueSearchResult): # type: ignore From 6948431e7477b9cab893a49f216f939891129434 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 18:51:04 +0000 Subject: [PATCH 333/358] chore(deps): bump github/codeql-action in the dependencies group Bumps the dependencies group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 3.29.2 to 3.29.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/181d5eefc20863364f96762470ba6f862bdef56b...d6bbdef45e766d081b84a2def353b0055f728d3e) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.29.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 047161af..b916d75e 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.24.9 + uses: github/codeql-action/upload-sarif@d6bbdef45e766d081b84a2def353b0055f728d3e # v3.24.9 with: sarif_file: results.sarif From b677265a3be4095c3d1a75aa55345f08e031669f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 18:58:26 +0000 Subject: [PATCH 334/358] chore(deps): bump super-linter/super-linter from 7.4.0 to 8.0.0 Bumps [super-linter/super-linter](https://github.com/super-linter/super-linter) from 7.4.0 to 8.0.0. - [Release notes](https://github.com/super-linter/super-linter/releases) - [Changelog](https://github.com/super-linter/super-linter/blob/main/CHANGELOG.md) - [Commits](https://github.com/super-linter/super-linter/compare/12150456a73e248bdc94d0794898f94e23127c88...5119dcd8011e92182ce8219d9e9efc82f16fddb6) --- updated-dependencies: - dependency-name: super-linter/super-linter dependency-version: 8.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/linter.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index 9a7a3df4..9069eb7b 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -30,7 +30,7 @@ jobs: run: | pip install -r requirements.txt -r requirements-test.txt - name: Lint Code Base - uses: super-linter/super-linter@12150456a73e248bdc94d0794898f94e23127c88 # v7.4.0 + uses: super-linter/super-linter@5119dcd8011e92182ce8219d9e9efc82f16fddb6 # v8.0.0 env: DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From c46b8513f39bb658d2ccf07ecf23ee8d252c954f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 19:24:55 +0000 Subject: [PATCH 335/358] chore(deps): bump mypy from 1.16.1 to 1.17.0 in the dependencies group Bumps the dependencies group with 1 update: [mypy](https://github.com/python/mypy). Updates `mypy` from 1.16.1 to 1.17.0 - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.16.1...v1.17.0) --- updated-dependencies: - dependency-name: mypy dependency-version: 1.17.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 3b294cb5..ee1e9b93 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,6 +1,6 @@ black==25.1.0 flake8==7.3.0 -mypy==1.16.1 +mypy==1.17.0 mypy-extensions==1.1.0 pylint==3.3.7 pytest==8.4.1 From e0357967dfab60c6d253d9168a8ac73e673ee7ab Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Mon, 21 Jul 2025 12:45:58 -0700 Subject: [PATCH 336/358] docs: specific descriptive link text Signed-off-by: Zack Koppert --- README.md | 4 ++-- docs/verify-token-access-to-repository.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 596bfc38..baddaba5 100644 --- a/README.md +++ b/README.md @@ -112,9 +112,9 @@ All feedback regarding our GitHub Actions, as a whole, should be communicated th - Do this by creating a [GitHub API token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic) with permissions to read the repository and write issues. - Then take the value of the API token you just created, and [create a repository secret](https://docs.github.com/en/actions/security-guides/encrypted-secrets) where the name of the secret is `GH_TOKEN` and the value of the secret the API token. - Then finally update the workflow file to use that repository secret by changing `GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}` to `GH_TOKEN: ${{ secrets.GH_TOKEN }}`. The name of the secret can really be anything. It just needs to match between when you create the secret name and when you refer to it in the workflow file. - - Help on verifying your token's access to your repository [here](docs/verify-token-access-to-repository.md) + - Help on verifying your token's access to your repository [in the docs directory](docs/verify-token-access-to-repository.md) 6. If you want the resulting issue with the metrics in it to appear in a different repository other than the one the workflow file runs in, update the line `token: ${{ secrets.GITHUB_TOKEN }}` with your own GitHub API token stored as a repository secret. - - This process is the same as described in the step above. More info on creating secrets can be found [here](https://docs.github.com/en/actions/security-guides/encrypted-secrets). + - This process is the same as described in the step above. More info on creating secrets can be found [in the GitHub docs security guide on encrypted secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets). 7. Commit the workflow file to the default branch (often `master` or `main`) 8. Wait for the action to trigger based on the `schedule` entry or manually trigger the workflow as shown in the [documentation](https://docs.github.com/en/actions/using-workflows/manually-running-a-workflow). diff --git a/docs/verify-token-access-to-repository.md b/docs/verify-token-access-to-repository.md index 25a4f1be..e6d7d54a 100644 --- a/docs/verify-token-access-to-repository.md +++ b/docs/verify-token-access-to-repository.md @@ -4,7 +4,7 @@ GitHub PAT token access can be confusing. Here's a quick way to test if the toke **Remove this snippet after you've verified your token.** -- Make sure you follow the token setup instructions [here](https://github.com/github/issue-metrics/tree/main?tab=readme-ov-file#use-as-a-github-action) first. +- Make sure you follow the token setup instructions [in the `README.md`](https://github.com/github/issue-metrics/tree/main?tab=readme-ov-file#use-as-a-github-action) first. - Replace `{owner/repo}` with your own repository information. From 3c75ad40ae98254439df9aa6957e18baf661a077 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 20:24:39 +0000 Subject: [PATCH 337/358] chore(deps): bump python from `6544e0e` to `4c2cf99` Bumps python from `6544e0e` to `4c2cf99`. --- updated-dependencies: - dependency-name: python dependency-version: 3.13-slim dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 26a58174..d54ffa4d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.13-slim@sha256:6544e0e002b40ae0f59bc3618b07c1e48064c4faed3a15ae2fbd2e8f663e8283 +FROM python:3.13-slim@sha256:4c2cf9917bd1cbacc5e9b07320025bdb7cdf2df7b0ceaccb55e9dd7e30987419 LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From dd2c4b8c2a817cc9ddc0d5850d408eeb145d1827 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 21:27:53 +0000 Subject: [PATCH 338/358] chore(deps): bump the dependencies group with 2 updates Bumps the dependencies group with 2 updates: [github/contributors](https://github.com/github/contributors) and [github/codeql-action](https://github.com/github/codeql-action). Updates `github/contributors` from 1.5.9 to 1.5.10 - [Release notes](https://github.com/github/contributors/releases) - [Commits](https://github.com/github/contributors/compare/4d90d92531d4c5775be5a70c119ca7c0be165964...34fad9a6770332457ef52dfefc24a282ff47e929) Updates `github/codeql-action` from 3.29.3 to 3.29.4 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/d6bbdef45e766d081b84a2def353b0055f728d3e...4e828ff8d448a8a6e532957b1811f387a63867e8) --- updated-dependencies: - dependency-name: github/contributors dependency-version: 1.5.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: github/codeql-action dependency-version: 3.29.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/contributor_report.yaml | 2 +- .github/workflows/scorecard.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/contributor_report.yaml b/.github/workflows/contributor_report.yaml index d15c1841..d31decf5 100644 --- a/.github/workflows/contributor_report.yaml +++ b/.github/workflows/contributor_report.yaml @@ -27,7 +27,7 @@ jobs: echo "END_DATE=$end_date" >> "$GITHUB_ENV" - name: Run contributor action - uses: github/contributors@4d90d92531d4c5775be5a70c119ca7c0be165964 # v1.5.9 + uses: github/contributors@34fad9a6770332457ef52dfefc24a282ff47e929 # v1.5.10 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} START_DATE: ${{ env.START_DATE }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index b916d75e..69682151 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@d6bbdef45e766d081b84a2def353b0055f728d3e # v3.24.9 + uses: github/codeql-action/upload-sarif@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.24.9 with: sarif_file: results.sarif From 5e0646ef30c121d4d8f40f89b91a788d0b3f0116 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 23:20:01 +0000 Subject: [PATCH 339/358] chore(deps): bump github/codeql-action in the dependencies group Bumps the dependencies group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 3.29.4 to 3.29.5 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/4e828ff8d448a8a6e532957b1811f387a63867e8...51f77329afa6477de8c49fc9c7046c15b9a4e79d) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.29.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 69682151..50a3a4a1 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.24.9 + uses: github/codeql-action/upload-sarif@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.24.9 with: sarif_file: results.sarif From 14419bb7695cf4d9afe2757ee7f2540d373b996d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Aug 2025 00:10:22 +0000 Subject: [PATCH 340/358] chore(deps): bump mypy from 1.17.0 to 1.17.1 in the dependencies group Bumps the dependencies group with 1 update: [mypy](https://github.com/python/mypy). Updates `mypy` from 1.17.0 to 1.17.1 - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.17.0...v1.17.1) --- updated-dependencies: - dependency-name: mypy dependency-version: 1.17.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index ee1e9b93..e3e43537 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,6 +1,6 @@ black==25.1.0 flake8==7.3.0 -mypy==1.17.0 +mypy==1.17.1 mypy-extensions==1.1.0 pylint==3.3.7 pytest==8.4.1 From 0b29e611550fd715f1dd1954c1a819ebc478ee2d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 01:29:55 +0000 Subject: [PATCH 341/358] chore(deps): bump the dependencies group with 3 updates Bumps the dependencies group with 3 updates: [github/ospo-reusable-workflows](https://github.com/github/ospo-reusable-workflows), [github/contributors](https://github.com/github/contributors) and [github/codeql-action](https://github.com/github/codeql-action). Updates `github/ospo-reusable-workflows` from 0.5.1 to 0.5.2 - [Release notes](https://github.com/github/ospo-reusable-workflows/releases) - [Changelog](https://github.com/github/ospo-reusable-workflows/blob/main/docs/release-image.md) - [Commits](https://github.com/github/ospo-reusable-workflows/compare/6f158f242fe68adb5a2698ef47e06dac07ac7e71...ebb4e218b75c6043139fd69a4c9bb5a465fb696b) Updates `github/contributors` from 1.5.10 to 1.5.11 - [Release notes](https://github.com/github/contributors/releases) - [Commits](https://github.com/github/contributors/compare/34fad9a6770332457ef52dfefc24a282ff47e929...69e531b620b7e5b0fad2e9823681607b54db447a) Updates `github/codeql-action` from 3.29.7 to 3.29.8 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/51f77329afa6477de8c49fc9c7046c15b9a4e79d...76621b61decf072c1cee8dd1ce2d2a82d33c17ed) --- updated-dependencies: - dependency-name: github/ospo-reusable-workflows dependency-version: 0.5.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: github/contributors dependency-version: 1.5.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: github/codeql-action dependency-version: 3.29.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/auto-labeler.yml | 2 +- .github/workflows/contributor_report.yaml | 2 +- .github/workflows/pr-title.yml | 2 +- .github/workflows/release.yml | 6 +++--- .github/workflows/scorecard.yml | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/auto-labeler.yml b/.github/workflows/auto-labeler.yml index 43fe7790..48d4f70c 100644 --- a/.github/workflows/auto-labeler.yml +++ b/.github/workflows/auto-labeler.yml @@ -11,7 +11,7 @@ jobs: permissions: contents: write pull-requests: write - uses: github/ospo-reusable-workflows/.github/workflows/auto-labeler.yaml@6f158f242fe68adb5a2698ef47e06dac07ac7e71 + uses: github/ospo-reusable-workflows/.github/workflows/auto-labeler.yaml@ebb4e218b75c6043139fd69a4c9bb5a465fb696b with: config-name: release-drafter.yml secrets: diff --git a/.github/workflows/contributor_report.yaml b/.github/workflows/contributor_report.yaml index d31decf5..123abb10 100644 --- a/.github/workflows/contributor_report.yaml +++ b/.github/workflows/contributor_report.yaml @@ -27,7 +27,7 @@ jobs: echo "END_DATE=$end_date" >> "$GITHUB_ENV" - name: Run contributor action - uses: github/contributors@34fad9a6770332457ef52dfefc24a282ff47e929 # v1.5.10 + uses: github/contributors@69e531b620b7e5b0fad2e9823681607b54db447a # v1.5.11 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} START_DATE: ${{ env.START_DATE }} diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index e06a1fbb..77afc54b 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -12,6 +12,6 @@ jobs: contents: read pull-requests: read statuses: write - uses: github/ospo-reusable-workflows/.github/workflows/pr-title.yaml@6f158f242fe68adb5a2698ef47e06dac07ac7e71 + uses: github/ospo-reusable-workflows/.github/workflows/pr-title.yaml@ebb4e218b75c6043139fd69a4c9bb5a465fb696b secrets: github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2f89b0a2..5cc0ca03 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: permissions: contents: write pull-requests: read - uses: github/ospo-reusable-workflows/.github/workflows/release.yaml@6f158f242fe68adb5a2698ef47e06dac07ac7e71 + uses: github/ospo-reusable-workflows/.github/workflows/release.yaml@ebb4e218b75c6043139fd69a4c9bb5a465fb696b with: publish: true release-config-name: release-drafter.yml @@ -25,7 +25,7 @@ jobs: packages: write id-token: write attestations: write - uses: github/ospo-reusable-workflows/.github/workflows/release-image.yaml@6f158f242fe68adb5a2698ef47e06dac07ac7e71 + uses: github/ospo-reusable-workflows/.github/workflows/release-image.yaml@ebb4e218b75c6043139fd69a4c9bb5a465fb696b with: image-name: ${{ github.repository_owner }}/issue_metrics full-tag: ${{ needs.release.outputs.full-tag }} @@ -40,7 +40,7 @@ jobs: permissions: contents: read discussions: write - uses: github/ospo-reusable-workflows/.github/workflows/release-discussion.yaml@6f158f242fe68adb5a2698ef47e06dac07ac7e71 + uses: github/ospo-reusable-workflows/.github/workflows/release-discussion.yaml@ebb4e218b75c6043139fd69a4c9bb5a465fb696b with: full-tag: ${{ needs.release.outputs.full-tag }} body: ${{ needs.release.outputs.body }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 50a3a4a1..8e75ea49 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.24.9 + uses: github/codeql-action/upload-sarif@76621b61decf072c1cee8dd1ce2d2a82d33c17ed # v3.24.9 with: sarif_file: results.sarif From 99209880fdfb60e3c5037fb3f058109e7f211644 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 06:17:27 -0500 Subject: [PATCH 342/358] chore(deps): bump python from `4c2cf99` to `6f79e7a` (#563) Bumps python from `4c2cf99` to `6f79e7a`. --- updated-dependencies: - dependency-name: python dependency-version: 3.13-slim dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d54ffa4d..07fb1a1d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.13-slim@sha256:4c2cf9917bd1cbacc5e9b07320025bdb7cdf2df7b0ceaccb55e9dd7e30987419 +FROM python:3.13-slim@sha256:6f79e7a10bb7d0b0a50534a70ebc78823f941fba26143ecd7e6c5dca9d7d7e8a LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ From 2fd323a2f81a70e719d1035f7b7c7535c5bc53ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:21:01 +0000 Subject: [PATCH 343/358] chore(deps): bump actions/checkout from 4.2.2 to 5.0.0 (#565) Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.2 to 5.0.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.2.2...v5.0.0) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 5.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker-image.yml | 2 +- .github/workflows/linter.yaml | 2 +- .github/workflows/python-package.yml | 2 +- .github/workflows/scorecard.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 3c2f8ec2..9f65a353 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -14,6 +14,6 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v5.0.0 - name: Build the Docker image run: docker build . --file Dockerfile --platform linux/amd64 --tag issue-metrics:"$(date +%s)" diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index 9069eb7b..fe5ec0d6 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -18,7 +18,7 @@ jobs: statuses: write steps: - name: Checkout Code - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 with: # Full git history is needed to get a proper # list of changed files within `super-linter` diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 2853bb19..07a7ff7b 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -21,7 +21,7 @@ jobs: python-version: [3.11, 3.12] steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v5.0.0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5.6.0 with: diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 8e75ea49..0b95c7e0 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -25,7 +25,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 with: persist-credentials: false From 65f43f43c5aeb79492241360d485df9fc362648f Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 Aug 2025 10:45:53 -0500 Subject: [PATCH 344/358] fix: Status and Created At column header/data alignment issue (#569) --- markdown_writer.py | 8 +-- test_column_order_fix.py | 123 +++++++++++++++++++++++++++++++++++++++ test_markdown_writer.py | 6 +- 3 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 test_column_order_fix.py diff --git a/markdown_writer.py b/markdown_writer.py index 7a88a8ac..67cc2417 100644 --- a/markdown_writer.py +++ b/markdown_writer.py @@ -75,10 +75,6 @@ def get_non_hidden_columns(labels) -> List[str]: if not hide_time_to_answer: columns.append("Time to answer") - hide_status = env_vars.hide_status - if not hide_status: - columns.append("Status") - enable_time_in_draft = env_vars.draft_pr_tracking if enable_time_in_draft: columns.append("Time in draft") @@ -91,6 +87,10 @@ def get_non_hidden_columns(labels) -> List[str]: if not hide_created_at: columns.append("Created At") + hide_status = env_vars.hide_status + if not hide_status: + columns.append("Status") + return columns diff --git a/test_column_order_fix.py b/test_column_order_fix.py new file mode 100644 index 00000000..c186ad7d --- /dev/null +++ b/test_column_order_fix.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 + +""" +Test to verify that the Status and Created At columns have their content aligned with headers. + +This test specifically validates the fix for issue #568 where the Status and Created At +columns had their data swapped. +""" + +import os +import unittest +from datetime import timedelta +from unittest.mock import patch + +from classes import IssueWithMetrics +from markdown_writer import get_non_hidden_columns, write_to_markdown + + +@patch.dict( + os.environ, + { + "SEARCH_QUERY": "is:open repo:user/repo", + "GH_TOKEN": "test_token", + "HIDE_CREATED_AT": "False", + "HIDE_STATUS": "False", + }, +) +class TestColumnOrderFix(unittest.TestCase): + """Test that Status and Created At columns have correct data.""" + + def test_status_and_created_at_columns_alignment(self): + """Test that Status and Created At columns show correct data values. + + This test specifically validates that: + 1. The Status column contains actual status values (not dates) + 2. The Created At column contains actual date values (not status) + """ + # Create test data with clearly distinguishable Status and Created At values + issues_with_metrics = [ + IssueWithMetrics( + title="Test Issue", + html_url="https://github.com/user/repo/issues/1", + author="testuser", + assignee="assignee1", + assignees=["assignee1"], + created_at="2023-01-01T00:00:00Z", # This should appear in Created At column + status="open", # This should appear in Status column + time_to_first_response=timedelta(days=1), + time_to_close=timedelta(days=2), + time_to_answer=timedelta(days=3), + ) + ] + + # Call the function + write_to_markdown( + issues_with_metrics=issues_with_metrics, + average_time_to_first_response=None, + average_time_to_close=None, + average_time_to_answer=None, + average_time_in_draft=None, + average_time_in_labels=None, + num_issues_opened=1, + num_issues_closed=0, + num_mentor_count=0, + labels=None, + search_query="is:issue is:open repo:user/repo", + hide_label_metrics=True, + hide_items_closed_count=False, + enable_mentor_count=False, + non_mentioning_links=False, + report_title="Test Report", + output_file="test_column_order.md", + ) + + # Read the generated markdown + with open("test_column_order.md", "r", encoding="utf-8") as file: + content = file.read() + + # The table should have the columns in the correct order + # and the data should be properly aligned + expected_header = ( + "| Title | URL | Assignee | Author | Time to first response | " + "Time to close | Time to answer | Created At | Status |" + ) + self.assertIn(expected_header, content) + + # Verify the data row has correct values in correct positions + # The Created At column should contain the date value + # The Status column should contain the status value + expected_row = ( + "| Test Issue | https://github.com/user/repo/issues/1 | " + "[assignee1](https://github.com/assignee1) | " + "[testuser](https://github.com/testuser) | 1 day, 0:00:00 | " + "2 days, 0:00:00 | 3 days, 0:00:00 | 2023-01-01T00:00:00Z | open |" + ) + self.assertIn(expected_row, content) + + # Clean up + os.remove("test_column_order.md") + + def test_get_non_hidden_columns_order(self): + """Test that get_non_hidden_columns returns columns in the correct order.""" + columns = get_non_hidden_columns(labels=None) + + # Find the indices of the Status and Created At columns + try: + created_at_index = columns.index("Created At") + status_index = columns.index("Status") + + # Status should come after Created At + self.assertGreater( + status_index, + created_at_index, + "Status column should come after Created At column", + ) + except ValueError: + # If one of the columns is hidden, that's fine, but we shouldn't get here + # given our environment variables + self.fail("Both Status and Created At columns should be present") + + +if __name__ == "__main__": + unittest.main() diff --git a/test_markdown_writer.py b/test_markdown_writer.py index 0fbcad37..c79536b5 100644 --- a/test_markdown_writer.py +++ b/test_markdown_writer.py @@ -137,7 +137,7 @@ def test_write_to_markdown(self): "| Number of items closed | 1 |\n" "| Total number of items created | 2 |\n\n" "| Title | URL | Assignee | Author | Time to first response | Time to close | " - "Time to answer | Status | Time in draft | Time spent in bug | Created At |\n" + "Time to answer | Time in draft | Time spent in bug | Created At | Status |\n" "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n" "| Issue 1 | https://github.com/user/repo/issues/1 | [charlie](https://github.com/charlie) | " "[alice](https://github.com/alice) | 1 day, 0:00:00 | 2 days, 0:00:00 | 3 days, 0:00:00 | " @@ -254,7 +254,7 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): "| Number of items closed | 1 |\n" "| Total number of items created | 2 |\n\n" "| Title | URL | Assignee | Author | Time to first response | Time to close | " - "Time to answer | Status | Time in draft | Time spent in bug | Created At |\n" + "Time to answer | Time in draft | Time spent in bug | Created At | Status |\n" "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n" "| Issue 1 | https://github.com/user/repo/issues/1 | [charlie](https://github.com/charlie) | " "[alice](https://github.com/alice) | 1 day, 0:00:00 | 2 days, 0:00:00 | 3 days, 0:00:00 | " @@ -405,7 +405,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): "| Number of items that remain open | 2 |\n" "| Number of most active mentors | 5 |\n" "| Total number of items created | 2 |\n\n" - "| Title | URL | Assignee | Author | Status | Created At |\n" + "| Title | URL | Assignee | Author | Created At | Status |\n" "| --- | --- | --- | --- | --- | --- |\n" "| Issue 1 | https://www.ghe.com/user/repo/issues/1 | [charlie](https://ghe.com/charlie) | " "[alice](https://ghe.com/alice) | -5 days, 0:00:00 | None |\n" From e9b5de2d907c7293b3608b8af3f4a731bf73683a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 Aug 2025 17:44:49 +0000 Subject: [PATCH 345/358] Initial plan From e517b06752f83df23aa29b7b21adda7ced379294 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 Aug 2025 17:53:59 +0000 Subject: [PATCH 346/358] Fix DRAFT_PR_TRACKING AttributeError by correcting event access pattern Co-authored-by: zkoppert <6935431+zkoppert@users.noreply.github.com> --- test_time_in_draft.py | 38 ++++++++++++++++++++++++++++++++------ time_in_draft.py | 2 +- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/test_time_in_draft.py b/test_time_in_draft.py index 24751477..1a3fc056 100644 --- a/test_time_in_draft.py +++ b/test_time_in_draft.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta from unittest.mock import MagicMock +import github3 import pytz from time_in_draft import get_stats_time_in_draft, measure_time_in_draft @@ -18,13 +19,14 @@ def setUp(self): Setup common test data and mocks. """ self.issue = MagicMock() + self.issue.issue = MagicMock(spec=github3.issues.Issue) self.issue.issue.state = "open" def test_time_in_draft_with_ready_for_review(self): """ Test measure_time_in_draft with one draft and review interval. """ - self.issue.events.return_value = [ + self.issue.issue.events.return_value = [ MagicMock( event="converted_to_draft", created_at=datetime(2021, 1, 1, tzinfo=pytz.utc), @@ -42,7 +44,7 @@ def test_time_in_draft_without_ready_for_review(self): """ Test measure_time_in_draft when ready_for_review_at is not provided and issue is still open. """ - self.issue.events.return_value = [ + self.issue.issue.events.return_value = [ MagicMock( event="converted_to_draft", created_at=datetime(2021, 1, 1, tzinfo=pytz.utc), @@ -59,7 +61,7 @@ def test_time_in_draft_multiple_intervals(self): """ Test measure_time_in_draft with multiple draft intervals. """ - self.issue.events.return_value = [ + self.issue.issue.events.return_value = [ MagicMock( event="converted_to_draft", created_at=datetime(2021, 1, 1, tzinfo=pytz.utc), @@ -85,7 +87,7 @@ def test_time_in_draft_ongoing_draft(self): """ Test measure_time_in_draft with an ongoing draft interval. """ - self.issue.events.return_value = [ + self.issue.issue.events.return_value = [ MagicMock( event="converted_to_draft", created_at=datetime(2021, 1, 1, tzinfo=pytz.utc), @@ -103,7 +105,7 @@ def test_time_in_draft_no_draft_events(self): """ Test measure_time_in_draft with no draft-related events. """ - self.issue.events.return_value = [] + self.issue.issue.events.return_value = [] result = measure_time_in_draft(self.issue) self.assertIsNone( result, "The result should be None when there are no draft events." @@ -113,7 +115,7 @@ def test_time_in_draft_without_ready_for_review_and_closed(self): """ Test measure_time_in_draft for a closed issue with an ongoing draft and ready_for_review_at is not provided. """ - self.issue.events.return_value = [ + self.issue.issue.events.return_value = [ MagicMock( event="converted_to_draft", created_at=datetime(2021, 1, 1, tzinfo=pytz.utc), @@ -126,6 +128,30 @@ def test_time_in_draft_without_ready_for_review_and_closed(self): "The result should be None for a closed issue with an ongoing draft.", ) + def test_time_in_draft_with_attribute_error_scenario(self): + """ + Test measure_time_in_draft to ensure it doesn't raise AttributeError when called + with issue structure similar to what get_per_issue_metrics passes. + This test reproduces the original bug scenario. + """ + # This simulates the actual issue structure passed from get_per_issue_metrics + issue_search_result = MagicMock() + issue_search_result.issue = MagicMock(spec=github3.issues.Issue) + issue_search_result.issue.state = "open" + issue_search_result.issue.events.return_value = [ + MagicMock( + event="converted_to_draft", + created_at=datetime(2021, 1, 1, tzinfo=pytz.utc), + ), + ] + + # This should NOT raise AttributeError: events + with unittest.mock.patch("time_in_draft.datetime") as mock_datetime: + mock_datetime.now.return_value = datetime(2021, 1, 4, tzinfo=pytz.utc) + result = measure_time_in_draft(issue_search_result) + expected = timedelta(days=3) + self.assertEqual(result, expected, "The time in draft should be 3 days.") + class TestGetStatsTimeInDraft(unittest.TestCase): """ diff --git a/time_in_draft.py b/time_in_draft.py index 92441456..98f01e7f 100644 --- a/time_in_draft.py +++ b/time_in_draft.py @@ -22,7 +22,7 @@ def measure_time_in_draft( returns: Union[timedelta, None]: Total time the pull request has spent in draft state. """ - events = issue.events() + events = issue.issue.events() draft_start = None total_draft_time = timedelta(0) From 80d86493c2edc9a175a9fb9565569dabf0e19435 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 Aug 2025 22:09:34 +0000 Subject: [PATCH 347/358] Fix linting issues: remove trailing whitespace in test file Co-authored-by: zkoppert <6935431+zkoppert@users.noreply.github.com> --- test_time_in_draft.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test_time_in_draft.py b/test_time_in_draft.py index 1a3fc056..8a442cc9 100644 --- a/test_time_in_draft.py +++ b/test_time_in_draft.py @@ -130,7 +130,7 @@ def test_time_in_draft_without_ready_for_review_and_closed(self): def test_time_in_draft_with_attribute_error_scenario(self): """ - Test measure_time_in_draft to ensure it doesn't raise AttributeError when called + Test measure_time_in_draft to ensure it doesn't raise AttributeError when called with issue structure similar to what get_per_issue_metrics passes. This test reproduces the original bug scenario. """ @@ -144,7 +144,7 @@ def test_time_in_draft_with_attribute_error_scenario(self): created_at=datetime(2021, 1, 1, tzinfo=pytz.utc), ), ] - + # This should NOT raise AttributeError: events with unittest.mock.patch("time_in_draft.datetime") as mock_datetime: mock_datetime.now.return_value = datetime(2021, 1, 4, tzinfo=pytz.utc) From 9f2ad7e5b0353ace5de24bc74187ed0ad21473d7 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 Aug 2025 08:17:17 -0500 Subject: [PATCH 348/358] fix: label metrics overcounting for closed issues when labels removed before closure (#571) * Initial plan * Fix label metrics overcounting bug and add comprehensive tests Co-authored-by: zkoppert <6935431+zkoppert@users.noreply.github.com> * Add 3 additional test cases for label metrics with creation-time labels and varied timeframes Co-authored-by: zkoppert <6935431+zkoppert@users.noreply.github.com> * Fix black linting issue with long function name formatting Co-authored-by: zkoppert <6935431+zkoppert@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: zkoppert <6935431+zkoppert@users.noreply.github.com> Co-authored-by: Zack Koppert --- labels.py | 5 +- test_labels.py | 175 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+), 1 deletion(-) diff --git a/labels.py b/labels.py index b9502b41..ee8656b5 100644 --- a/labels.py +++ b/labels.py @@ -85,11 +85,14 @@ def get_label_metrics(issue: github3.issues.Issue, labels: List[str]) -> dict: if label in labeled: # if the issue is closed, add the time from the issue creation to the closed_at time if issue.state == "closed": + # Only add the final (closed_at - created_at) span if the label was still applied at closure. + if label_last_event_type.get(label) != "labeled": + continue label_metrics[label] += datetime.fromisoformat( issue.closed_at ) - datetime.fromisoformat(issue.created_at) else: - # skip label if last labeling event is 'unlabled' and issue is still open + # skip label if last labeling event is 'unlabeled' and issue is still open if label_last_event_type[label] == "unlabeled": continue diff --git a/test_labels.py b/test_labels.py index 9c49b2b8..bea6edfa 100644 --- a/test_labels.py +++ b/test_labels.py @@ -93,6 +93,181 @@ def test_get_label_metrics_closed_issue_labeled_past_closed_at(self): metrics = get_label_metrics(self.issue, labels) self.assertEqual(metrics["foo"], None) + def test_get_label_metrics_closed_issue_label_removed_before_closure(self): + """Test get_label_metrics for a closed issue where label was removed before closure""" + # Create a mock issue that reproduces the problem scenario: + # Issue created: day 0 (2021-01-01) + # Label added: day 5 (2021-01-06) + # Label removed: day 10 (2021-01-11) + # Issue closed: day 15 (2021-01-16) + # Expected duration: 5 days (from day 5 to day 10) + + issue = MagicMock() + issue.issue = MagicMock(spec=github3.issues.Issue) + issue.created_at = "2021-01-01T00:00:00Z" + issue.closed_at = "2021-01-16T00:00:00Z" # 15 days after creation + issue.state = "closed" + issue.issue.events.return_value = [ + MagicMock( + event="labeled", + label={"name": "test-label"}, + created_at=datetime(2021, 1, 6, tzinfo=pytz.UTC), # day 5 + ), + MagicMock( + event="unlabeled", + label={"name": "test-label"}, + created_at=datetime(2021, 1, 11, tzinfo=pytz.UTC), # day 10 + ), + ] + + labels = ["test-label"] + metrics = get_label_metrics(issue, labels) + + # Should be 5 days (from day 5 to day 10), not 15 days (full issue duration) + expected_duration = timedelta(days=5) + self.assertEqual(metrics["test-label"], expected_duration) + + def test_get_label_metrics_closed_issue_label_remains_through_closure(self): + """Test get_label_metrics for a closed issue where label remains applied through closure""" + # Test scenario where label is applied and never removed: + # Issue created: day 0 (2021-01-01) + # Label added: day 2 (2021-01-03) + # Issue closed: day 10 (2021-01-11) + # Expected duration: 10 days (from issue creation to closure) + + issue = MagicMock() + issue.issue = MagicMock(spec=github3.issues.Issue) + issue.created_at = "2021-01-01T00:00:00Z" + issue.closed_at = "2021-01-11T00:00:00Z" # 10 days after creation + issue.state = "closed" + issue.issue.events.return_value = [ + MagicMock( + event="labeled", + label={"name": "stays-applied"}, + created_at=datetime(2021, 1, 3, tzinfo=pytz.UTC), # day 2 + ), + # No unlabel event - label remains applied + ] + + labels = ["stays-applied"] + metrics = get_label_metrics(issue, labels) + + # Should be 8 days (from day 2 when label was applied to day 10 when issue closed) + expected_duration = timedelta(days=8) + self.assertEqual(metrics["stays-applied"], expected_duration) + + def test_get_label_metrics_label_applied_at_creation_and_removed_before_closure( + self, + ): + """Test get_label_metrics for a label applied at issue creation and removed before closure""" + # Test scenario where label is applied at creation and later removed: + # Issue created: day 0 (2021-01-01) with label applied + # Label removed: day 7 (2021-01-08) + # Issue closed: day 20 (2021-01-21) + # Expected duration: 7 days (from creation to removal) + + issue = MagicMock() + issue.issue = MagicMock(spec=github3.issues.Issue) + issue.created_at = "2021-01-01T00:00:00Z" + issue.closed_at = "2021-01-21T00:00:00Z" # 20 days after creation + issue.state = "closed" + issue.issue.events.return_value = [ + MagicMock( + event="labeled", + label={"name": "creation-label"}, + created_at=datetime(2021, 1, 1, tzinfo=pytz.UTC), # day 0 - at creation + ), + MagicMock( + event="unlabeled", + label={"name": "creation-label"}, + created_at=datetime(2021, 1, 8, tzinfo=pytz.UTC), # day 7 + ), + ] + + labels = ["creation-label"] + metrics = get_label_metrics(issue, labels) + + # Should be 7 days (from creation to removal), not 20 days (full issue duration) + expected_duration = timedelta(days=7) + self.assertEqual(metrics["creation-label"], expected_duration) + + def test_get_label_metrics_label_applied_at_creation_remains_through_closure(self): + """Test get_label_metrics for a label applied at creation and kept through closure""" + # Test scenario where label is applied at creation and never removed: + # Issue created: day 0 (2021-01-01) with label applied + # Issue closed: day 30 (2021-01-31) + # Expected duration: 30 days (full issue duration) + + issue = MagicMock() + issue.issue = MagicMock(spec=github3.issues.Issue) + issue.created_at = "2021-01-01T00:00:00Z" + issue.closed_at = "2021-01-31T00:00:00Z" # 30 days after creation + issue.state = "closed" + issue.issue.events.return_value = [ + MagicMock( + event="labeled", + label={"name": "permanent-label"}, + created_at=datetime(2021, 1, 1, tzinfo=pytz.UTC), # day 0 - at creation + ), + # No unlabel event - label remains applied + ] + + labels = ["permanent-label"] + metrics = get_label_metrics(issue, labels) + + # Should be 30 days (full issue duration since label was applied at creation) + expected_duration = timedelta(days=30) + self.assertEqual(metrics["permanent-label"], expected_duration) + + def test_get_label_metrics_multiple_labels_different_timeframes(self): + """Test get_label_metrics with multiple labels having different application patterns and longer timeframes""" + # Test scenario with multiple labels and longer timeframes: + # Issue created: day 0 (2021-01-01) + # Label A applied: day 0 (at creation) + # Label B applied: day 14 (2021-01-15) + # Label A removed: day 21 (2021-01-22) + # Label B removed: day 35 (2021-02-05) + # Issue closed: day 60 (2021-03-02) + # Expected: Label A = 21 days, Label B = 21 days + + issue = MagicMock() + issue.issue = MagicMock(spec=github3.issues.Issue) + issue.created_at = "2021-01-01T00:00:00Z" + issue.closed_at = "2021-03-02T00:00:00Z" # 60 days after creation + issue.state = "closed" + issue.issue.events.return_value = [ + MagicMock( + event="labeled", + label={"name": "label-a"}, + created_at=datetime(2021, 1, 1, tzinfo=pytz.UTC), # day 0 - at creation + ), + MagicMock( + event="labeled", + label={"name": "label-b"}, + created_at=datetime(2021, 1, 15, tzinfo=pytz.UTC), # day 14 + ), + MagicMock( + event="unlabeled", + label={"name": "label-a"}, + created_at=datetime(2021, 1, 22, tzinfo=pytz.UTC), # day 21 + ), + MagicMock( + event="unlabeled", + label={"name": "label-b"}, + created_at=datetime(2021, 2, 5, tzinfo=pytz.UTC), # day 35 + ), + ] + + labels = ["label-a", "label-b"] + metrics = get_label_metrics(issue, labels) + + # Label A: 21 days (from day 0 to day 21) + # Label B: 21 days (from day 14 to day 35) + expected_duration_a = timedelta(days=21) + expected_duration_b = timedelta(days=21) + self.assertEqual(metrics["label-a"], expected_duration_a) + self.assertEqual(metrics["label-b"], expected_duration_b) + class TestGetAverageTimeInLabels(unittest.TestCase): """Unit tests for get_stats_time_in_labels""" From 8198e455a45bcf36c264dac9c611ae8b42337d69 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 00:45:28 +0000 Subject: [PATCH 349/358] chore(deps): bump types-pytz from 2025.2.0.20250516 to 2025.2.0.20250809 Bumps [types-pytz](https://github.com/typeshed-internal/stub_uploader) from 2025.2.0.20250516 to 2025.2.0.20250809. - [Commits](https://github.com/typeshed-internal/stub_uploader/commits) --- updated-dependencies: - dependency-name: types-pytz dependency-version: 2025.2.0.20250809 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index e3e43537..fd3844ca 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -5,5 +5,5 @@ mypy-extensions==1.1.0 pylint==3.3.7 pytest==8.4.1 pytest-cov==6.2.1 -types-pytz==2025.2.0.20250516 +types-pytz==2025.2.0.20250809 types-requests==2.32.4.20250611 From aa316544c437e82710c55de0fe789b5b1c629c47 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 05:07:13 +0000 Subject: [PATCH 350/358] chore(deps): bump types-requests from 2.32.4.20250611 to 2.32.4.20250809 Bumps [types-requests](https://github.com/typeshed-internal/stub_uploader) from 2.32.4.20250611 to 2.32.4.20250809. - [Commits](https://github.com/typeshed-internal/stub_uploader/commits) --- updated-dependencies: - dependency-name: types-requests dependency-version: 2.32.4.20250809 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index fd3844ca..1defdd49 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -6,4 +6,4 @@ pylint==3.3.7 pytest==8.4.1 pytest-cov==6.2.1 types-pytz==2025.2.0.20250809 -types-requests==2.32.4.20250611 +types-requests==2.32.4.20250809 From 740926ce6e5341914ae473ee48d5a0dcf2ec223d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 15:19:55 -0500 Subject: [PATCH 351/358] chore(deps): bump the dependencies group across 1 directory with 2 updates (#579) Bumps the dependencies group with 2 updates in the / directory: [requests](https://github.com/psf/requests) and [pylint](https://github.com/pylint-dev/pylint). Updates `requests` from 2.32.4 to 2.32.5 - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.32.4...v2.32.5) Updates `pylint` from 3.3.7 to 3.3.8 - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v3.3.7...v3.3.8) --- updated-dependencies: - dependency-name: requests dependency-version: 2.32.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: pylint dependency-version: 3.3.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-test.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index 1defdd49..c0be31ec 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,7 +2,7 @@ black==25.1.0 flake8==7.3.0 mypy==1.17.1 mypy-extensions==1.1.0 -pylint==3.3.7 +pylint==3.3.8 pytest==8.4.1 pytest-cov==6.2.1 types-pytz==2025.2.0.20250809 diff --git a/requirements.txt b/requirements.txt index 2d48a848..5ba9ab59 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ github3.py==4.0.1 numpy==2.2.4 python-dotenv==1.1.1 pytz==2025.2 -requests==2.32.4 +requests==2.32.5 From 6e412dd6ba6b32853f0213450fb1ba5154c267f5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 15:20:13 -0500 Subject: [PATCH 352/358] chore(deps): bump github/codeql-action in the dependencies group (#574) Bumps the dependencies group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 3.29.8 to 3.29.10 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/76621b61decf072c1cee8dd1ce2d2a82d33c17ed...96f518a34f7a870018057716cc4d7a5c014bd61c) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.29.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 0b95c7e0..c9ca2c6e 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@76621b61decf072c1cee8dd1ce2d2a82d33c17ed # v3.24.9 + uses: github/codeql-action/upload-sarif@96f518a34f7a870018057716cc4d7a5c014bd61c # v3.24.9 with: sarif_file: results.sarif From c0a53522b8ca7eeb6bceeac441ce665a64b37861 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 20:21:26 +0000 Subject: [PATCH 353/358] chore(deps): bump python from `6f79e7a` to `27f90d7` (#578) * chore(deps): bump python from `6f79e7a` to `27f90d7` Bumps python from `6f79e7a` to `27f90d7`. --- updated-dependencies: - dependency-name: python dependency-version: 3.13-slim dependency-type: direct:production ... Signed-off-by: dependabot[bot] * fix: git version in Dockerfile Signed-off-by: jmeridth <35014+jmeridth@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] Signed-off-by: jmeridth <35014+jmeridth@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: jmeridth <35014+jmeridth@users.noreply.github.com> --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 07fb1a1d..b12d6ce5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.13-slim@sha256:6f79e7a10bb7d0b0a50534a70ebc78823f941fba26143ecd7e6c5dca9d7d7e8a +FROM python:3.13-slim@sha256:27f90d79cc85e9b7b2560063ef44fa0e9eaae7a7c3f5a9f74563065c5477cc24 LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ @@ -17,7 +17,7 @@ COPY requirements.txt *.py /action/workspace/ RUN python3 -m pip install --no-cache-dir -r requirements.txt \ && apt-get -y update \ - && apt-get -y install --no-install-recommends git=1:2.39.5-0+deb12u2 \ + && apt-get -y install --no-install-recommends git=1:2.47.2-0.2 \ && rm -rf /var/lib/apt/lists/* CMD ["/action/workspace/issue_metrics.py"] From e53376f8b964250d19b0aa7d846f5ff7e93dbfc9 Mon Sep 17 00:00:00 2001 From: jmeridth <35014+jmeridth@users.noreply.github.com> Date: Wed, 20 Aug 2025 17:49:24 -0500 Subject: [PATCH 354/358] feat: copilot files (instructions and setup steps) This ensures that when we have the copilot agent writing PRs, it sets up its environment correctly and understands our standards for coding. - Copilot Instructions file ([docs](https://docs.github.com/en/enterprise-cloud@latest/copilot/how-tos/agents/copilot-coding-agent/best-practices-for-using-copilot-to-work-on-tasks#adding-custom-instructions-to-your-repository)) - Copilot Setup Steps ([docs](https://docs.github.com/en/copilot/how-tos/agents/copilot-coding-agent/customizing-the-development-environment-for-copilot-coding-agent#preinstalling-tools-or-dependencies-in-copilots-environment)) Already added to [Evergreen](https://github.com/github/evergreen/pull/382) Signed-off-by: jmeridth <35014+jmeridth@users.noreply.github.com> --- .github/copilot-instructions.md | 30 ++++++++++++++++++ .github/workflows/copilot-setup-steps.yml | 38 +++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 .github/copilot-instructions.md create mode 100644 .github/workflows/copilot-setup-steps.yml diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..fd519888 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,30 @@ +# Copilot Instructions + +This is a GitHub Action that searches for issues/pull requests/discussions in a repository, measures several metrics, and generates a report in form of a GitHub issue. The issues/pull requests/discussions to search for can be filtered by using a search query. + +## Code Standards + +### Required Before Each Commit + +- Run `make lint` before committing any changes to ensure proper code linting and formatting. + +### Development Flow + +- Lint: `make lint` +- Test: `make test` + +## Repository Structure + +- `Makefile`: Contains commands for linting, testing, and other tasks +- `requirements.txt`: Python dependencies for the project +- `requirements-test.txt`: Python dependencies for testing +- `README.md`: Project documentation and setup instructions +- `setup.py`: Python package setup configuration +- `test_*.py`: Python test files matching the naming convention for test discovery + +## Key Guidelines + +1. Follow Python best practices and idiomatic patterns +2. Maintain existing code structure and organization +3. Write unit tests for new functionality. +4. Document changes to environment variables in the `README.md` file. diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 00000000..cddb8d9d --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,38 @@ +name: "Copilot Setup Steps" + +# Automatically run the setup steps when they are changed to allow for easy validation, and +# allow manual testing through the repository's "Actions" tab +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + +# Set the permissions to the lowest permissions possible needed for your steps. +# Copilot will be given its own token for its operations. +permissions: + # If you want to clone the repository as part of your setup steps, for example to install dependencies, you'll need the `contents: read` permission. If you don't clone the repository in your setup steps, Copilot will do this for you automatically after the steps complete. + contents: read + +jobs: + # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. + copilot-setup-steps: + runs-on: ubuntu-latest + + # You can define any steps you want, and they will run before the agent starts. + # If you do not check out your code, Copilot will do this for you. + steps: + - name: Checkout code + uses: actions/checkout@v5.0.0 + + - name: Set up Python + uses: actions/setup-python@v5.6.0 + with: + python-version: 3.12 + + - name: Install dependencies + run: | + pip install -r requirements.txt -r requirements-test.txt From f1b344c4826e14251cfcbeb7bc17b94812705dc3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Sep 2025 23:42:49 +0000 Subject: [PATCH 355/358] Initial plan From f19c4a67d28a1299bfeb21a2d620595f8aaf425a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Sep 2025 23:48:43 +0000 Subject: [PATCH 356/358] Fix TypeError for ghost users in pull request processing Co-authored-by: zkoppert <6935431+zkoppert@users.noreply.github.com> --- issue_metrics.py | 17 +++++++---- test_issue_metrics.py | 65 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index 0c86912c..fd99f09c 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -139,11 +139,18 @@ def get_per_issue_metrics( # Check if issue is actually a pull request pull_request, ready_for_review_at = None, None if issue.issue.pull_request_urls: # type: ignore - pull_request = issue.issue.pull_request() # type: ignore - ready_for_review_at = get_time_to_ready_for_review(issue, pull_request) - if env_vars.draft_pr_tracking: - issue_with_metrics.time_in_draft = measure_time_in_draft( - issue=issue + try: + pull_request = issue.issue.pull_request() # type: ignore + ready_for_review_at = get_time_to_ready_for_review( + issue, pull_request + ) + if env_vars.draft_pr_tracking: + issue_with_metrics.time_in_draft = measure_time_in_draft( + issue=issue + ) + except TypeError as e: + print( + f"An error occurred processing review comments. Perhaps the review contains a ghost user. {e}" ) if env_vars.hide_time_to_first_response is False: diff --git a/test_issue_metrics.py b/test_issue_metrics.py index 5a6e9de6..3da9f1e5 100644 --- a/test_issue_metrics.py +++ b/test_issue_metrics.py @@ -365,6 +365,71 @@ def test_get_per_issue_metrics_with_ignore_users(self): expected_issues_with_metrics[0].time_to_close, ) + @patch.dict( + os.environ, + { + "GH_TOKEN": "test_token", + "SEARCH_QUERY": "is:pr is:open repo:user/repo", + }, + ) + def test_get_per_issue_metrics_with_ghost_user_pull_request(self): + """ + Test that the function handles TypeError when a pull request + contains a ghost user (deleted account) gracefully. + """ + # Create mock data for a pull request that will cause TypeError on pull_request() + mock_issue = MagicMock( + title="PR with Ghost User", + html_url="https://github.com/user/repo/pull/1", + user={"login": "existing_user"}, + state="open", + comments=0, + created_at="2023-01-01T00:00:00Z", + closed_at=None, + ) + + # Mock the issue to have pull_request_urls (indicating it's a PR) + mock_issue.issue.pull_request_urls = [ + "https://api.github.com/repos/user/repo/pulls/1" + ] + + # Make pull_request() raise TypeError (simulating ghost user scenario) + mock_issue.issue.pull_request.side_effect = TypeError( + "'NoneType' object is not subscriptable" + ) + mock_issue.issue.comments.return_value = [] + mock_issue.issue.assignee = None + mock_issue.issue.assignees = None + + issues = [mock_issue] + + # Mock the measure functions to avoid additional complexities + with unittest.mock.patch( # type:ignore + "issue_metrics.measure_time_to_first_response", + return_value=timedelta(days=1), + ), unittest.mock.patch( # type:ignore + "issue_metrics.measure_time_to_close", return_value=None + ): + # Call the function and verify it doesn't crash + ( + result_issues_with_metrics, + result_num_issues_open, + result_num_issues_closed, + ) = get_per_issue_metrics( + issues, + env_vars=get_env_vars(test=True), + ) + + # Verify the function completed successfully despite the TypeError + self.assertEqual(len(result_issues_with_metrics), 1) + self.assertEqual(result_num_issues_open, 1) + self.assertEqual(result_num_issues_closed, 0) + + # Verify the issue was processed with pull_request as None + issue_metric = result_issues_with_metrics[0] + self.assertEqual(issue_metric.title, "PR with Ghost User") + self.assertEqual(issue_metric.author, "existing_user") + class TestDiscussionMetrics(unittest.TestCase): """Test suite for the discussion_metrics function.""" From cbda2ec1a7d4f837625e9112ec6ef4c1c86b3085 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Sep 2025 04:46:58 +0000 Subject: [PATCH 357/358] Fix spacing in type ignore comments Co-authored-by: zkoppert <6935431+zkoppert@users.noreply.github.com> --- test_issue_metrics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test_issue_metrics.py b/test_issue_metrics.py index 3da9f1e5..d44b67cd 100644 --- a/test_issue_metrics.py +++ b/test_issue_metrics.py @@ -404,10 +404,10 @@ def test_get_per_issue_metrics_with_ghost_user_pull_request(self): issues = [mock_issue] # Mock the measure functions to avoid additional complexities - with unittest.mock.patch( # type:ignore + with unittest.mock.patch( # type: ignore "issue_metrics.measure_time_to_first_response", return_value=timedelta(days=1), - ), unittest.mock.patch( # type:ignore + ), unittest.mock.patch( # type: ignore "issue_metrics.measure_time_to_close", return_value=None ): # Call the function and verify it doesn't crash From 6fa9041b3ea7b6a9dafbdc388d25d86d06b2b6af Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Sep 2025 00:20:37 -0500 Subject: [PATCH 358/358] fix: time_in_draft calculation for pull requests initially created as drafts (#587) * Initial plan * Fix time_in_draft calculation for PRs initially created as drafts Co-authored-by: zkoppert <6935431+zkoppert@users.noreply.github.com> * perf: reduce duplicate loops Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Zack Koppert --------- Signed-off-by: Zack Koppert Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: zkoppert <6935431+zkoppert@users.noreply.github.com> Co-authored-by: Zack Koppert Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- issue_metrics.py | 2 +- test_time_in_draft.py | 52 +++++++++++++++++++++++++++++++++++++++++++ time_in_draft.py | 50 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) diff --git a/issue_metrics.py b/issue_metrics.py index fd99f09c..a7dd5041 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -146,7 +146,7 @@ def get_per_issue_metrics( ) if env_vars.draft_pr_tracking: issue_with_metrics.time_in_draft = measure_time_in_draft( - issue=issue + issue=issue, pull_request=pull_request ) except TypeError as e: print( diff --git a/test_time_in_draft.py b/test_time_in_draft.py index 8a442cc9..8a4b9503 100644 --- a/test_time_in_draft.py +++ b/test_time_in_draft.py @@ -128,6 +128,58 @@ def test_time_in_draft_without_ready_for_review_and_closed(self): "The result should be None for a closed issue with an ongoing draft.", ) + def test_time_in_draft_initially_created_as_draft(self): + """ + Test measure_time_in_draft with a PR initially created as draft. + """ + # Set up issue created_at time + self.issue.issue.created_at = "2021-01-01T00:00:00Z" + + # Mock events with only ready_for_review (no converted_to_draft) + self.issue.issue.events.return_value = [ + MagicMock( + event="ready_for_review", + created_at=datetime(2021, 1, 3, tzinfo=pytz.utc), + ), + ] + + # Mock pull request object + mock_pull_request = MagicMock() + + result = measure_time_in_draft(self.issue, mock_pull_request) + expected = timedelta(days=2) + self.assertEqual( + result, + expected, + "The time in draft should be 2 days for initially draft PR.", + ) + + def test_time_in_draft_initially_created_as_draft_still_open(self): + """ + Test measure_time_in_draft with a PR initially created as draft and still in draft. + """ + # Set up issue created_at time + self.issue.issue.created_at = "2021-01-01T00:00:00Z" + + # Mock events with no ready_for_review events (still draft) + self.issue.issue.events.return_value = [] + + # Mock pull request object indicating it's currently draft + mock_pull_request = MagicMock() + mock_pull_request.draft = True + + with unittest.mock.patch("time_in_draft.datetime") as mock_datetime: + # Keep the real datetime class but only mock the now() method + mock_datetime.fromisoformat = datetime.fromisoformat + mock_datetime.now.return_value = datetime(2021, 1, 4, tzinfo=pytz.utc) + result = measure_time_in_draft(self.issue, mock_pull_request) + expected = timedelta(days=3) + self.assertEqual( + result, + expected, + "The time in draft should be 3 days for initially draft PR still in draft.", + ) + def test_time_in_draft_with_attribute_error_scenario(self): """ Test measure_time_in_draft to ensure it doesn't raise AttributeError when called diff --git a/time_in_draft.py b/time_in_draft.py index 98f01e7f..a663cc5a 100644 --- a/time_in_draft.py +++ b/time_in_draft.py @@ -13,11 +13,13 @@ def measure_time_in_draft( issue: github3.issues.Issue, + pull_request: Union[github3.pulls.PullRequest, None] = None, ) -> Union[timedelta, None]: """If a pull request has had time in the draft state, return the cumulative amount of time it was in draft. args: issue (github3.issues.Issue): A GitHub issue which has been pre-qualified as a pull request. + pull_request (github3.pulls.PullRequest, optional): The pull request object. returns: Union[timedelta, None]: Total time the pull request has spent in draft state. @@ -26,6 +28,54 @@ def measure_time_in_draft( draft_start = None total_draft_time = timedelta(0) + # Check if PR was initially created as draft + pr_created_at = None + + try: + if pull_request is None: + pull_request = issue.issue.pull_request() + + pr_created_at = datetime.fromisoformat( + issue.issue.created_at.replace("Z", "+00:00") + ) + + # Look for ready_for_review events to determine if PR was initially draft + ready_for_review_events = [] + converted_to_draft_events = [] + for event in events: + if event.event == "ready_for_review": + ready_for_review_events.append(event) + elif event.event == "converted_to_draft": + converted_to_draft_events.append(event) + + # If there are ready_for_review events, check if PR was initially draft + if ready_for_review_events: + first_ready_event = min(ready_for_review_events, key=lambda x: x.created_at) + prior_draft_events = [ + e + for e in converted_to_draft_events + if e.created_at < first_ready_event.created_at + ] + + if not prior_draft_events: + # PR was initially created as draft, calculate time from creation to first ready_for_review + total_draft_time += first_ready_event.created_at - pr_created_at + + # If there are no ready_for_review events but the PR is currently draft, it might be initially draft and still open + elif not ready_for_review_events and not converted_to_draft_events: + # Check if PR is currently draft and open + if ( + hasattr(pull_request, "draft") + and pull_request.draft + and issue.issue.state == "open" + ): + # PR was initially created as draft and is still draft + draft_start = pr_created_at + + except (AttributeError, ValueError, TypeError): + # If we can't get PR info, fall back to original logic + pass + for event in events: if event.event == "converted_to_draft": draft_start = event.created_at