Skip to content

Commit

Permalink
Add missing .generate-reports.py file (#224)
Browse files Browse the repository at this point in the history
This hotfix will add a missing report generation script for all templates, that caused
the report generation github workflow to fail.

Co-authored-by: Sven F <sven.fillinger@qbic.uni-tuebingen.de>
  • Loading branch information
KochTobi and sven1103 authored Nov 6, 2020
1 parent de29e20 commit 2cfa883
Show file tree
Hide file tree
Showing 9 changed files with 282 additions and 14 deletions.
2 changes: 1 addition & 1 deletion .cookietemple.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ full_name: Lukas Heumos
email: lukas.heumos@posteo.net
project_name: qube
project_short_description: QBiC's internal project template collection.
version: 2.6.0
version: 2.6.1
license: MIT
command_line_interface: Click
use_pytest: y
14 changes: 14 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ Changelog
This project adheres to `Semantic Versioning <https://semver.org/>`_.


2.6.1 (2020-11-06)
------------------

**Added**

* Add report generation script to common files

**Fixed**

**Dependencies**

**Deprecated**


2.6.0 (2020-10-27)
------------------

Expand Down
2 changes: 1 addition & 1 deletion cookietemple.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 2.6.0
current_version = 2.6.1

[bumpversion_files_whitelisted]
setup_file = setup.py
Expand Down
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@
# the built documents.
#
# The short X.Y version.
version = '2.6.0'
version = '2.6.1'
# The full version, including alpha/beta/rc tags.
release = '2.6.0'
release = '2.6.1'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down
2 changes: 1 addition & 1 deletion qube/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

__author__ = """Lukas Heumos"""
__email__ = 'lukas.heumos@posteo.net'
__version__ = '2.6.0'
__version__ = '2.6.1'
14 changes: 7 additions & 7 deletions qube/create/templates/available_templates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ cli:
java:
name: Java Commandline Tool
handle: cli-java
version: 1.0.3
version: 1.0.4
available libraries: picocli
short description: Tools to be used using a console (e.g., postman-cli)
long description: A template for Java based tools, which provide a command line user interface to the user.
Expand All @@ -11,7 +11,7 @@ lib:
java:
name: Generic Java Library
handle: lib-java
version: 1.0.4
version: 1.0.5
available libraries: none
short description: Generic library for Java based projects
long description: A template for Java based libraries, which provide functionalities to other projects.
Expand All @@ -21,7 +21,7 @@ lib:
groovy:
name: Generic Groovy Library
handle: lib-groovy
version: 1.0.0
version: 1.0.1
available libraries: none
short description: Generic library for Groovy based projects
long description: A template for Groovy based libraries, which provide functionalities to other projects.
Expand All @@ -32,7 +32,7 @@ gui:
java:
name: JavaFX based GUI
handle: gui-java
version: 1.0.2
version: 1.0.3
available libraries: JavaFX
short description: JavaFX based graphical user interfaces for Java based projects
long description: A template for Java based tools, which require a graphical user interface for the user to interact with.
Expand All @@ -41,7 +41,7 @@ service:
java:
name: Service
handle: service-java
version: 1.0.3
version: 1.0.4
available libraries: core-utils-lib
short description: Service, which once started stays 'active' until shutdown
long description: Services are similar to command-line tools in their structure, but once a service has been started, it stays 'active' until shutdown.
Expand All @@ -50,15 +50,15 @@ portlet:
groovy:
name: Portlet
handle: portlet-groovy
version: 1.0.3
version: 1.0.4
available libraries: Vaadin
short description: Vaadin based portlet, which build the foundation of QBiC's web presence
long description: Portlets are web-based components, which process requests and create dynamic content.
The backend and frontend of this template is based on Vaadin 8 (https://vaadin.com/vaadin-8).
groovy_osgi:
name: Portlet
handle: portlet-groovy_osgi
version: 1.0.3
version: 1.0.4
available libraries: Vaadin
short description: Vaadin based portlet, which build the foundation of QBiC's web presence
long description: Portlets are web-based components, which process requests and create dynamic content.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# fancy comments come at a cost!

# Script to generate a Maven site and push reports to a branch (gh-pages by default).
# This script assumes that both git and Maven have been installed and that the following environment variables
# are defined:
# - REPORTS_GITHUB_ACCESS_TOKEN: GitHub personal access token used to push generated reports
# - REPORTS_GITHUB_USERNAME: username used to push generated reports
#
# Yes, these could be passed as arguments, but Travis log would print them out.

# Output of this script is to populate the gh-pages branch with the reports generated by running "mvn site".
# The structure of the generated reports is similar to:
#
# (branch gh-pages) # pages_branch option
# reports # base_output_dir option
# ├── development # output_dir positional argument
# │  ├── index.html
# │  ├── pmd.html
# │ ├── jacoco.html
# │ └── ...
# │  
# ├── 1.0.0 # output_dir positional argument
# │  ├── index.html
# │  ├── pmd.html
# │ ├── jacoco.html
# │ └── ...
# │  
# ├── 1.0.1 # output_dir positional argument
# │  ├── index.html
# │  ├── pmd.html
# │ ├── jacoco.html
# │ └── ...
# │  
# └── 2.0.0 # output_dir positional argument
#   ├── index.html
#   ├── pmd.html
# ├── jacoco.html
# └── ...
#
# So only one "development" version of the reports is maintained, while reports for all
# tagged commits--assumed to be releases--are maintained on the gh-pages branch.
#
# The content of each of the folders is whatever Maven generates on the target/site folder.


import argparse, os, shutil, subprocess, tempfile, sys, re

# folder where maven outputs reports generated by running "mvn site"
MAVEN_SITE_DIR = os.path.join('target', 'site')
# base directory where reports will be copied to
BASE_REPORT_DIR = 'reports'
# credentials are given via environment variables
TOKEN_ENV_VARIABLE_NAME = 'REPORTS_GITHUB_ACCESS_TOKEN'
# compiled regex to match files that should not be deleted when cleaning the working folder (in gh-pages)
UNTOUCHABLE_FILES_MATCHER = re.compile('^\.git.*')
# regex to validate output folder
REPORTS_VERSION_REGEX = '^(development|[vV]?\d+\.\d+\.\d+)$'


# parses arguments and does the thing
def main():
parser = argparse.ArgumentParser(description='QBiC Javadoc Generator.', prog='generate-javadocs.py', formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('-s', '--site-dir', default=MAVEN_SITE_DIR,
help='Directory where Maven reports are found (output of running \'mvn site\').')
parser.add_argument('-b', '--base-output-dir', default=BASE_REPORT_DIR,
help='Base directory where the reports will be copied.')
parser.add_argument('-p', '--pages-branch', default="gh-pages",
help='Name of the git branch on which the reports will be pushed.')
parser.add_argument('-a', '--access-token-var-name', default=TOKEN_ENV_VARIABLE_NAME,
help='Name of the environment variable holding the GitHub personal access token used to push changes in reports.')
parser.add_argument('-r', '--validation-regex', default=REPORTS_VERSION_REGEX,
help='Regular expression to validate output_dir; it is assumed that report folders are named after a version.')
parser.add_argument('--dry-run', action='store_true',
help='If present, no changes to the remote repository (git commit/push) will be executed.')
parser.add_argument('--skip-cleanup', action='store_true',
help='Whether cleanup tasks (removing cloned repos) should be skipped.')
parser.add_argument('output_dir',
help='Name of the folder, relative to the base output directory, where reports will be copied to. \
This folder will be first cleared of its contents before the generated reports are copied. \
Recommended values are: "development" or a valid release version string (e.g., 1.0.1)')
parser.add_argument('repo_slug', help='Slug of the repository for which reports are being built.')
parser.add_argument('commit_message', nargs='+', help='Message(s) to use when committing changes.')
args = parser.parse_args()

# check that the required environment variables have been defined
try:
validateArguments(args)
except Exception as e:
print('Error: {}'.format(str(e)), file=sys.stderr)
exit(1)

# since this will run on Travis, we cannot assume that we can change the current local repo without breaking anything
# the safest way would be to clone this same repository on a temporary folder and leave the current local repo alone
working_dir = tempfile.mkdtemp()
clone_self(working_dir, args)

# reports are available only in a specific branch
force_checkout_pages_branch(working_dir, args)

# since new branches have a parent commit, we have to remove everything but:
# * important files (e.g., .git)
# * the base output directory (args.base_output_dir)
# otherwise, the newly created gh-pages branch will contain other non-report files!
# also, it is a good idea to remove everything, since we don't want lingering unused report files
remove_unneeded_files(working_dir, args)

# move rports to their place
prepare_report_dir(working_dir, args)

# add, commit, push
push_to_pages_branch(working_dir, args)

# clean up
if args.skip_cleanup:
print('Skipping cleanup of working folder {}'.format(working_dir))
else:
print('Removing working folder {}'.format(working_dir))
shutil.rmtree(working_dir)


# Sanity check
def validateArguments(args):
# check that the required environment variables are present
if not args.access_token_var_name in os.environ:
raise Exception('At least one of the required environment variables is missing. See comments on .generate-reports.py for further information.')

# check if the name of the output_dir matches the regex
regex = re.compile(args.validation_regex)
if not regex.match(args.output_dir):
raise Exception('The provided output directory for the reports, {}, is not valid. It must match the regex {}'.format(args.output_dir, args.validation_regex))

# check that the reports are where they should be (you never know!)
if not os.path.exists(args.site_dir) or not os.path.isdir(args.site_dir):
raise Exception('Maven site folder {} does not exist or is not a directory.'.format(args.site_dir))


# Clones this repo into the passed working directory, credentials are used because OAuth has a bigger quota
# plus, we will be pushing changes to gh-pages branch
def clone_self(working_dir, args, exit_if_fail=True):
execute(['git', 'clone', 'https://{}:x-oauth-basic@github.com/{}'.format(os.environ[args.access_token_var_name], args.repo_slug), working_dir],
'Could not clone {} in directory {}'.format(args.repo_slug, working_dir), exit_if_fail)


# Checks out the branch where reports reside (gh-pages)
def force_checkout_pages_branch(working_dir, args):
# we need to add the gh-pages branch if it doesn't exist (git checkout -b gh-pages),
# but if gh-pages already exists, we need to checkout (git checkout gh-pages), luckily,
# "git checkout branch" fails if branch doesn't exist
print('Changing to branch {}'.format(args.pages_branch))
try:
execute(['git', '-C', working_dir, 'checkout', args.pages_branch], exit_if_fail=False)
except:
execute(['git', '-C', working_dir, 'checkout', '-b', args.pages_branch], 'Could not create branch {}'.format(args.pages_branch))


# Goes through the all files/folders (non-recursively) and deletes them using 'git rm'.
# Files that should not be deleted are ignored
def remove_unneeded_files(working_dir, args):
print('Cleaning local repository ({}) of non-reports files'.format(working_dir))
for f in os.listdir(working_dir):
if should_delete(f, args):
# instead of using OS calls to delete files/folders, use git rm to stage deletions
print(' Deleting {} from {} branch'.format(f, args.pages_branch))
execute(['git', '-C', working_dir, 'rm', '-r', '--ignore-unmatch', f], 'Could not remove {}.'.format(f))
# files that are not part of the repository aren't removed by git and the --ignore-unmatch flag makes
# git be nice so it doesn't exit with errors, so we need to force-remove them
force_delete(os.path.join(working_dir, f))
else:
print(' Ignoring file/folder {}'.format(f))


# Prepares the report output directory, first by clearing it and then by moving the contents of target/site into it
def prepare_report_dir(working_dir, args):
report_output_dir = os.path.join(working_dir, args.base_output_dir, args.output_dir)
if os.path.exists(report_output_dir):
if not os.path.isdir(report_output_dir):
print('WARNING: Output destination {} exists and is not a directory.'.format(report_output_dir), file=sys.stderr)
# remove the object from git
print('Removing {}'.format(report_output_dir))
execute(['git', '-C', working_dir, 'rm', '-r', '--ignore-unmatch', os.path.join(args.base_output_dir, args.output_dir)],
'Could not remove {}.'.format(report_output_dir))
# just in case git doesn't remove the file (if it wasn't tracked, for instance), force deletion using OS calls
force_delete(report_output_dir)
# we know the output folder doesn't exist, so we can recreate it
print('Creating {}'.format(report_output_dir))
os.makedirs(report_output_dir)

# accidentally the whole target/site folder (well, yes, but actually, no, because we need only its contents)
print('Moving contents of {} to {}'.format(args.site_dir, report_output_dir))
for f in os.listdir(args.site_dir):
print(' Moving {}'.format(f))
shutil.move(os.path.join(args.site_dir, f), report_output_dir)


# Adds, commits and pushes changes
def push_to_pages_branch(working_dir, args):
if args.dry_run:
print('(running in dry run mode) Local/remote repository will not be modified')
else:
# add changes to the index
print('Staging changes for commit')
execute(['git', '-C', working_dir, 'add', '.'], 'Could not stage reports for commit.')

# build the git-commit command and commit changes
print('Pushing changes upstream')
git_commit_command = ['git', '-C', working_dir, 'commit']
for commit_message in args.commit_message:
git_commit_command.extend(['-m', commit_message])
execute(git_commit_command, 'Could not commit changes')

# https://www.youtube.com/watch?v=vCadcBR95oU
execute(['git', '-C', working_dir, 'push', '-u', 'origin', args.pages_branch], 'Could not push changes using provided credentials.')


# Whether it is safe to delete the given path, we won't delete important files/folders (such as .git)
# or the base output directory
def should_delete(path, args):
return not UNTOUCHABLE_FILES_MATCHER.match(path) and path != args.base_output_dir


# Forcefully deletes recursively the passed file/folder using OS calls
def force_delete(file):
if os.path.exists(file):
if os.path.isdir(file):
shutil.rmtree(file)
else:
os.remove(file)


# Executes an external command
# stderr/stdout are hidden to avoid leaking credentials into log files in Travis, so it might be a pain in the butt to debug, sorry, but safety first!
# if exit_if_fail is set to True, this method will print minimal stacktrace information and exit if a failure is encountered, otherwise, an exception
# will be thrown (this is useful if an error will be handled by the invoking method)
def execute(command, error_message='Error encountered while executing command', exit_if_fail=True):
# do not print the command, stderr or stdout! this might expose usernames/passwords/tokens!
try:
subprocess.run(command, check=True, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
except:
if exit_if_fail:
stack = traceback.extract_stack()
try:
print('{}\n Error originated at file {}, line {}'.format(error_message, stack[-2].filename, stack[-2].lineno), file=sys.stderr)
except:
print('{}\n No information about the originating call is available.'.format(error_message), file=sys.stderr)
exit(1)
else:
raise Exception()



if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
universal = 1

[flake8]
exclude = docs
exclude = docs, .generate-reports.py
max-line-length = 160

[aliases]
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,6 @@ def walker(base, *paths):
test_suite='tests',
tests_require=test_requirements,
url='https://github.com/qbicsoftware/qube',
version='2.6.0',
version='2.6.1',
zip_safe=False,
)

0 comments on commit 2cfa883

Please sign in to comment.