From dcae456af6a0f478d5962a6e97247f3fbbd46649 Mon Sep 17 00:00:00 2001 From: Quirin Pamp Date: Tue, 11 Jan 2022 18:57:35 +0100 Subject: [PATCH 1/2] Update release branch CI to use GitHub issues [noissue] --- .ci/ansible/smash-config.json | 3 +- .ci/scripts/validate_commit_message.py | 34 +++---- .github/template_gitref | 2 +- .github/workflows/ci.yml | 6 +- .github/workflows/nightly.yml | 14 ++- .github/workflows/release.yml | 14 +-- .github/workflows/scripts/before_install.sh | 2 - .github/workflows/scripts/before_script.sh | 5 + .github/workflows/scripts/check_commit.sh | 2 + .github/workflows/scripts/docs-publisher.py | 21 +++++ .github/workflows/scripts/publish_docs.sh | 2 +- .github/workflows/scripts/release.py | 91 +------------------ .../stage-changelog-for-default-branch.py | 64 +++++++++++++ template_config.yml | 8 +- 14 files changed, 141 insertions(+), 127 deletions(-) create mode 100755 .github/workflows/scripts/stage-changelog-for-default-branch.py diff --git a/.ci/ansible/smash-config.json b/.ci/ansible/smash-config.json index 84a607bda..cd0e7d7ba 100644 --- a/.ci/ansible/smash-config.json +++ b/.ci/ansible/smash-config.json @@ -5,7 +5,8 @@ "password" ], "selinux enabled": false, - "version": "3" + "version": "3", + "aiohttp_fixtures_origin": "172.18.0.1" }, "hosts": [ { diff --git a/.ci/scripts/validate_commit_message.py b/.ci/scripts/validate_commit_message.py index 8bfcdca33..67001f7f2 100755 --- a/.ci/scripts/validate_commit_message.py +++ b/.ci/scripts/validate_commit_message.py @@ -11,38 +11,32 @@ from pathlib import Path -import requests +import os +import warnings +from github import Github NO_ISSUE = "[noissue]" CHANGELOG_EXTS = [".feature", ".bugfix", ".doc", ".removal", ".misc", ".deprecation"] -KEYWORDS = ["fixes", "closes", "re", "ref"] -STATUSES = ["NEW", "ASSIGNED", "POST", "MODIFIED"] -REDMINE_URL = "https://pulp.plan.io" +KEYWORDS = ["fixes", "closes"] sha = sys.argv[1] -project = "pulp_deb" message = subprocess.check_output(["git", "log", "--format=%B", "-n 1", sha]).decode("utf-8") +g = Github(os.environ.get("GITHUB_TOKEN")) +repo = g.get_repo("pulp/pulp_deb") def __check_status(issue): - response = requests.get(f"{REDMINE_URL}/issues/{issue}.json") - response.raise_for_status() - bug_json = response.json() - status = bug_json["issue"]["status"]["name"] - if status not in STATUSES: - sys.exit( - "Error: issue #{issue} has invalid status of {status}. Status must be one of " - "{statuses}.".format(issue=issue, status=status, statuses=", ".join(STATUSES)) + gi = repo.get_issue(int(issue)) + if gi.pull_request: + sys.exit(f"Error: issue #{issue} is a pull request.") + if gi.closed_at and "cherry picked from commit" not in message: + warnings.warn( + "When backporting, make sure to have 'cherry picked from commit' in the commit message." ) - - if project: - project_id = bug_json["issue"]["project"]["id"] - project_json = requests.get(f"{REDMINE_URL}/projects/{project_id}.json").json() - if project_json["project"]["identifier"] != project: - sys.exit(f"Error: issue {issue} is not in the {project} project.") + sys.exit(f"Error: issue #{issue} is closed.") def __check_changelog(issue): @@ -53,6 +47,8 @@ def __check_changelog(issue): for match in matches: if match.suffix not in CHANGELOG_EXTS: sys.exit(f"Invalid extension for changelog entry '{match}'.") + if match.suffix == ".feature" and "cherry picked from commit" in message: + sys.exit(f"Can not backport '{match}' as it is a feature.") print("Checking commit message for {sha}.".format(sha=sha[0:7])) diff --git a/.github/template_gitref b/.github/template_gitref index 80920e7a1..4bafce4e9 100644 --- a/.github/template_gitref +++ b/.github/template_gitref @@ -1 +1 @@ -2021.08.26-33-gc2a15f3 +2021.08.26-51-g538d663 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9fba9079b..b672dc11d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,9 +93,12 @@ jobs: echo ::group::HTTPIE pip install httpie echo ::endgroup:: - echo "TEST=${{ matrix.env.TEST }}" >> $GITHUB_ENV echo "HTTPIE_CONFIG_DIR=$GITHUB_WORKSPACE/.ci/assets/httpie/" >> $GITHUB_ENV + - name: Set environment variables + run: | + echo "TEST=${{ matrix.env.TEST }}" >> $GITHUB_ENV + - name: Before Install run: .github/workflows/scripts/before_install.sh @@ -147,6 +150,7 @@ jobs: GITHUB_REPO_SLUG: ${{ github.repository }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_CONTEXT: ${{ github.event.pull_request.commits_url }} + REDIS_DISABLED: ${{ contains('', matrix.env.TEST) }} - name: Setting secrets if: github.event_name != 'pull_request' diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 68bfbdabf..f351513eb 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -12,6 +12,7 @@ on: # * is a special character in YAML so you have to quote this string # runs at 3:00 UTC daily - cron: '00 3 * * *' + workflow_dispatch: jobs: test: @@ -43,9 +44,12 @@ jobs: echo ::group::HTTPIE pip install httpie echo ::endgroup:: - echo "TEST=${{ matrix.env.TEST }}" >> $GITHUB_ENV echo "HTTPIE_CONFIG_DIR=$GITHUB_WORKSPACE/.ci/assets/httpie/" >> $GITHUB_ENV + - name: Set environment variables + run: | + echo "TEST=${{ matrix.env.TEST }}" >> $GITHUB_ENV + - name: Before Install run: .github/workflows/scripts/before_install.sh @@ -92,6 +96,7 @@ jobs: GITHUB_REPO_SLUG: ${{ github.repository }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_CONTEXT: ${{ github.event.pull_request.commits_url }} + REDIS_DISABLED: ${{ contains('', matrix.env.TEST) }} - name: Setting secrets @@ -136,7 +141,6 @@ jobs: with: name: ruby-client.tar path: ruby-client.tar - - name: Upload built docs if: ${{ env.TEST == 'docs' }} uses: actions/upload-artifact@v2 @@ -184,9 +188,12 @@ jobs: echo ::group::HTTPIE pip install httpie echo ::endgroup:: - echo "TEST=${{ matrix.env.TEST }}" >> $GITHUB_ENV echo "HTTPIE_CONFIG_DIR=$GITHUB_WORKSPACE/.ci/assets/httpie/" >> $GITHUB_ENV + - name: Set environment variables + run: | + echo "TEST=${{ matrix.env.TEST }}" >> $GITHUB_ENV + - name: Install python dependencies run: | echo ::group::PYDEPS @@ -244,6 +251,7 @@ jobs: GITHUB_REPO_SLUG: ${{ github.repository }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_CONTEXT: ${{ github.event.pull_request.commits_url }} + REDIS_DISABLED: ${{ contains('', matrix.env.TEST) }} - name: Setting secrets diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c150d298d..28c834543 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -115,9 +115,12 @@ jobs: echo ::group::HTTPIE pip install httpie echo ::endgroup:: - echo "TEST=${{ matrix.env.TEST }}" >> $GITHUB_ENV echo "HTTPIE_CONFIG_DIR=$GITHUB_WORKSPACE/.ci/assets/httpie/" >> $GITHUB_ENV + - name: Set environment variables + run: | + echo "TEST=${{ matrix.env.TEST }}" >> $GITHUB_ENV + - name: Before Install run: .github/workflows/scripts/before_install.sh @@ -160,6 +163,7 @@ jobs: GITHUB_REPO_SLUG: ${{ github.repository }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_CONTEXT: ${{ github.event.pull_request.commits_url }} + REDIS_DISABLED: ${{ contains('', matrix.env.TEST) }} - name: Setting secrets @@ -208,7 +212,6 @@ jobs: with: name: ruby-client.tar path: ruby-client.tar - - name: Upload built docs if: ${{ env.TEST == 'docs' }} uses: actions/upload-artifact@v2 @@ -316,8 +319,7 @@ jobs: - name: Publish client to rubygems run: bash .github/workflows/scripts/publish_client_gem.sh - - name: Update Redmine - run: bash .ci/scripts/update_redmine.sh + - name: Create release on GitHub run: bash .github/workflows/scripts/create_release_from_tag.sh ${{ github.event.inputs.release }} @@ -325,8 +327,8 @@ jobs: - name: Cleanup repository before making changelog PR run: rm -rf .lock generation pulp_deb_client* *-client.tar pulp_deb.tar todo web docs.tar - - name: Stage changelog for master branch - run: python .github/workflows/scripts/stage-changelog-for-master.py ${{ github.event.inputs.release }} + - name: Stage changelog for main branch + run: python .github/workflows/scripts/stage-changelog-for-default-branch.py ${{ github.event.inputs.release }} - name: Create Pull Request for Changelog uses: peter-evans/create-pull-request@v3 diff --git a/.github/workflows/scripts/before_install.sh b/.github/workflows/scripts/before_install.sh index a63974b66..cc4b7ca1e 100755 --- a/.github/workflows/scripts/before_install.sh +++ b/.github/workflows/scripts/before_install.sh @@ -130,8 +130,6 @@ then echo "Failed to install amazon.aws" exit $s fi - -sed -i -e 's/DEBUG = False/DEBUG = True/' pulpcore/pulpcore/app/settings.py # Patch DJANGO_ALLOW_ASYNC_UNSAFE out of the pulpcore tasking_system # Don't let it fail. Be opportunistic. sed -i -e '/DJANGO_ALLOW_ASYNC_UNSAFE/d' pulpcore/pulpcore/tasking/entrypoint.py || true diff --git a/.github/workflows/scripts/before_script.sh b/.github/workflows/scripts/before_script.sh index baac10022..ad35f2916 100755 --- a/.github/workflows/scripts/before_script.sh +++ b/.github/workflows/scripts/before_script.sh @@ -34,6 +34,11 @@ if [[ "$TEST" == 'pulp' || "$TEST" == 'performance' || "$TEST" == 'upgrade' || " cmd_prefix dnf install -yq lsof which dnf-plugins-core fi +if [[ "${REDIS_DISABLED:-false}" == true ]]; then + cmd_prefix bash -c "s6-svc -d /var/run/s6/services/redis" + echo "The Redis service was disabled for $TEST" +fi + if [[ -f $POST_BEFORE_SCRIPT ]]; then source $POST_BEFORE_SCRIPT fi diff --git a/.github/workflows/scripts/check_commit.sh b/.github/workflows/scripts/check_commit.sh index 2b8c54c63..fa5c93754 100755 --- a/.github/workflows/scripts/check_commit.sh +++ b/.github/workflows/scripts/check_commit.sh @@ -15,6 +15,8 @@ set -euv echo ::group::REQUESTS pip3 install requests +pip3 install pygithub + echo ::endgroup:: for sha in $(curl -H "Authorization: token $GITHUB_TOKEN" $GITHUB_CONTEXT | jq '.[].sha' | sed 's/"//g') diff --git a/.github/workflows/scripts/docs-publisher.py b/.github/workflows/scripts/docs-publisher.py index e735ea867..a25688860 100755 --- a/.github/workflows/scripts/docs-publisher.py +++ b/.github/workflows/scripts/docs-publisher.py @@ -167,6 +167,27 @@ def main(): exit_code = subprocess.call(rsync_command, cwd=docs_directory) if exit_code != 0: raise RuntimeError("An error occurred while pushing docs.") + # publish to docs.pulpproject.org/en/3.y/ + version_components = branch.split(".") + x_y_version = "{}.{}".format(version_components[0], version_components[1]) + make_directory_with_rsync(["en", x_y_version]) + remote_path_arg = "%s@%s:%sen/%s/" % ( + USERNAME, + HOSTNAME, + SITE_ROOT, + x_y_version, + ) + rsync_command = [ + "rsync", + "-avzh", + "--delete", + "--omit-dir-times", + local_path_arg, + remote_path_arg, + ] + exit_code = subprocess.call(rsync_command, cwd=docs_directory) + if exit_code != 0: + raise RuntimeError("An error occurred while pushing docs.") # publish to docs.pulpproject.org/en/3.y.z/ make_directory_with_rsync(["en", branch]) remote_path_arg = "%s@%s:%sen/%s/" % (USERNAME, HOSTNAME, SITE_ROOT, branch) diff --git a/.github/workflows/scripts/publish_docs.sh b/.github/workflows/scripts/publish_docs.sh index a68e2a439..de9d58f6f 100755 --- a/.github/workflows/scripts/publish_docs.sh +++ b/.github/workflows/scripts/publish_docs.sh @@ -19,7 +19,7 @@ chmod 600 ~/.ssh/pulp-infra echo "docs.pulpproject.org,8.43.85.236 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGXG+8vjSQvnAkq33i0XWgpSrbco3rRqNZr0SfVeiqFI7RN/VznwXMioDDhc+hQtgVhd6TYBOrV07IMcKj+FAzg=" >> /home/runner/.ssh/known_hosts chmod 644 /home/runner/.ssh/known_hosts -pip3 install -r doc_requirements.txt +pip3 install packaging export PYTHONUNBUFFERED=1 export DJANGO_SETTINGS_MODULE=pulpcore.app.settings diff --git a/.github/workflows/scripts/release.py b/.github/workflows/scripts/release.py index 60a069635..eaa073b7c 100755 --- a/.github/workflows/scripts/release.py +++ b/.github/workflows/scripts/release.py @@ -20,70 +20,6 @@ from packaging.requirements import Requirement -from collections import defaultdict -from pathlib import Path -from redminelib import Redmine -from redminelib.exceptions import ResourceAttrError, ResourceSetIndexError -import json - -REDMINE_API_KEY = os.environ["REDMINE_API_KEY"] -REDMINE_URL = "https://pulp.plan.io" -REDMINE_QUERY_URL = f"{REDMINE_URL}/issues?set_filter=1&status_id=*&issue_id=" -REDMINE_PROJECT = "pulp_deb" - - -def validate_and_update_redmine_data(redmine_query_url, redmine_issues, release_version): - """Validate redmine milestone.""" - error_messages = [] - redmine = Redmine("https://pulp.plan.io", key=REDMINE_API_KEY) - redmine_project = redmine.project.get(REDMINE_PROJECT) - if redmine_project is None: - error_messages.append(f"Redmine project {REDMINE_PROJECT} not found.") - try: - milestone = redmine_project.versions.filter(name=release_version)[0] - except ResourceSetIndexError: - error_messages.append(f"Milestone {release_version} not found.") - - stats = defaultdict(list) - for issue in redmine_issues: - redmine_issue = redmine.issue.get(int(issue)) - - project_name = redmine_issue.project.name - stats[f"project_{project_name.lower().replace(' ', '_')}"].append(issue) - if project_name != redmine_project.name: - stats["wrong_project"].append(issue) - - status = redmine_issue.status.name - if "CLOSE" not in status and status != "MODIFIED": - stats["status_not_modified"].append(issue) - - try: - issue_milestone = redmine_issue.fixed_version - if redmine_issue.fixed_version.id != milestone.id: - stats["wrong_milestone"].append(issue) - stats[f"milestone_{issue_milestone.name}"].append(issue) - except ResourceAttrError: - redmine.issue.update(redmine_issue.id, fixed_version_id=milestone.id) - stats["added_to_milestone"].append(issue) - - print(f"\n\nRedmine stats: {json.dumps(stats, indent=2)}") - if stats.get("wrong_project"): - error_messages.append( - f"One or more issues are associated to the wrong project {stats['wrong_project']}" - ) - if stats.get("status_not_modified"): - error_messages.append(f"One or more issues are not MODIFIED {stats['status_not_modified']}") - if stats.get("wrong_milestone"): - error_messages.append( - f"One or more issues are associated to the wrong milestone {stats['wrong_milestone']}" - ) - - if error_messages: - error_messages.append(f"Verify at {redmine_query_url}") - raise RuntimeError("\n".join(error_messages)) - - return f"{milestone.url}.json" - async def get_package_from_pypi(package_name, plugin_path): """ @@ -120,18 +56,6 @@ async def get_package_from_pypi(package_name, plugin_path): def create_release_commits(repo, release_version, plugin_path): """Build changelog, set version, commit, bump to next dev version, commit.""" - issues_to_close = set() - for filename in Path(f"{plugin_path}/CHANGES").rglob("*"): - if filename.stem.isdigit(): - issue = filename.stem - issues_to_close.add(issue) - - issues = ",".join(issues_to_close) - redmine_final_query = f"{REDMINE_QUERY_URL}{issues}" - milestone_url = validate_and_update_redmine_data( - redmine_final_query, issues_to_close, release_version - ) - # First commit: changelog os.system(f"towncrier --yes --version {release_version}") git = repo.git @@ -148,18 +72,7 @@ def create_release_commits(repo, release_version, plugin_path): git.add(f"{plugin_path}/requirements.txt") git.add(f"{plugin_path}/.bumpversion.cfg") - git.commit( - "-m", - "\n".join( - [ - f"Release {release_version}", - "", - f"Redmine Query: {redmine_final_query}", - f"Redmine Milestone: {milestone_url}", - "[noissue]", - ] - ), - ) + git.commit("-m", f"Release {release_version}\n\n[noissue]") sha = repo.head.object.hexsha short_sha = git.rev_parse(sha, short=7) @@ -181,8 +94,6 @@ def create_release_commits(repo, release_version, plugin_path): git.add(f"{plugin_path}/.bumpversion.cfg") git.commit("-m", f"Bump to {new_dev_version}\n\n[noissue]") - print(f"\n\nRedmine query of issues to close:\n{redmine_final_query}") - print(f"Release commit == {short_sha}") print(f"All changes were committed on branch: release_{release_version}") return sha diff --git a/.github/workflows/scripts/stage-changelog-for-default-branch.py b/.github/workflows/scripts/stage-changelog-for-default-branch.py new file mode 100755 index 000000000..7d06e635a --- /dev/null +++ b/.github/workflows/scripts/stage-changelog-for-default-branch.py @@ -0,0 +1,64 @@ +# WARNING: DO NOT EDIT! +# +# This file was generated by plugin_template, and is managed by it. Please use +# './plugin-template --github pulp_deb' to update this file. +# +# For more info visit https://github.com/pulp/plugin_template + +import argparse +import os +import textwrap + +from git import Repo +from git.exc import GitCommandError + + +helper = textwrap.dedent( + """\ + Stage the changelog for a release on main branch. + + Example: + $ python .github/workflows/scripts/stage-changelog-for-default-branch.py 3.4.0 + + """ +) + +parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, description=helper) + +parser.add_argument( + "release_version", + type=str, + help="The version string for the release.", +) + +args = parser.parse_args() + +release_version_arg = args.release_version + +release_path = os.path.dirname(os.path.abspath(__file__)) +plugin_path = release_path.split("/.github")[0] + +print(f"\n\nRepo path: {plugin_path}") +repo = Repo(plugin_path) + +changelog_commit = None +# Look for a commit with the requested release version +for commit in repo.iter_commits(): + if f"{release_version_arg} changelog" == commit.message.split("\n")[0]: + changelog_commit = commit + break + +if not changelog_commit: + raise RuntimeError("Changelog commit for {release_version_arg} was not found.") + +git = repo.git +git.stash() +git.checkout("origin/main") +try: + git.cherry_pick(changelog_commit.hexsha) +except GitCommandError: + git.add("CHANGES/") + # Don't try opening an editor for the commit message + with git.custom_environment(GIT_EDITOR="true"): + git.cherry_pick("--continue") +git.reset("origin/main") diff --git a/template_config.yml b/template_config.yml index ebe46d4fe..1e7e4eaca 100644 --- a/template_config.yml +++ b/template_config.yml @@ -1,10 +1,11 @@ # This config represents the latest values used when running the plugin-template. Any settings that # were not present before running plugin-template have been added with their default values. -# generated with plugin_template@2021.08.26-20-gefc11dd +# generated with plugin_template@2021.08.26-51-g538d663 additional_plugins: [] additional_repos: [] +aiohttp_fixtures_origin: 172.18.0.1 black: true check_commit_message: true check_gettext: true @@ -20,10 +21,11 @@ deploy_client_to_rubygems: true deploy_daily_client_to_pypi: true deploy_daily_client_to_rubygems: true deploy_to_pypi: true +disabled_redis_runners: [] docker_fixtures: true docs_test: true flake8: true -issue_tracker: redmine +issue_tracker: github noissue_marker: '[noissue]' plugin_app_label: deb plugin_camel: PulpDeb @@ -65,6 +67,6 @@ test_fips_nightly: false test_performance: false test_released_plugin_with_next_pulpcore_release: false test_s3: true -update_redmine: true +update_redmine: false upgrade_range: [] From 12028891d8f64cb6e6d6d4517f3782fbf561bfc2 Mon Sep 17 00:00:00 2001 From: Quirin Pamp Date: Thu, 13 Jan 2022 11:35:06 +0100 Subject: [PATCH 2/2] Update some pulp.plan.io URLs to point at GitHub issues [noissue] --- CONTRIBUTING.rst | 2 +- README.md | 2 +- docs/external_references.rst | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index f00e540c3..95465670d 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -2,7 +2,7 @@ Contributing ================================================================================ .. _towncrier tool: https://github.com/hawkowl/towncrier -.. _pulp_deb issue tracker: https://pulp.plan.io/projects/pulp_deb/issues/ +.. _pulp_deb issue tracker: https://github.com/pulp/pulp_deb/issues To contribute to the ``pulp_deb`` plugin follow this process: diff --git a/README.md b/README.md index c6916f4f7..2d91eddab 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ The most important places: [1]: https://pulpproject.org [2]: https://docs.pulpproject.org/pulp_deb/ -[3]: https://pulp.plan.io/projects/pulp_deb/issues/ +[3]: https://github.com/pulp/pulp_deb/issues [4]: https://github.com/pulp/pulp_deb [5]: https://pypi.org/project/pulp-deb/ [6]: https://pypi.org/project/pulp-deb-client/ diff --git a/docs/external_references.rst b/docs/external_references.rst index 97edc6fd0..0e1ea704c 100644 --- a/docs/external_references.rst +++ b/docs/external_references.rst @@ -17,7 +17,7 @@ .. _pulpcore plugin API deprecation policy: https://docs.pulpproject.org/pulpcore/plugins/plugin-writer/concepts/index.html#plugin-api-stability-and-deprecation-policy .. _pulp_deb issue tracker: - https://pulp.plan.io/projects/pulp_deb/issues/ + https://github.com/pulp/pulp_deb/issues .. _pulp_deb milestone: https://pulp.plan.io/projects/pulp_deb/roadmap .. _pulpcore release guide: diff --git a/pyproject.toml b/pyproject.toml index bb86685f2..6407cac4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ filename = "CHANGES.rst" directory = "CHANGES/" title_format = "{version} ({project_date})" template = "CHANGES/.TEMPLATE.rst" -issue_format = "`#{issue} `_" +issue_format = "`#{issue} `_" [[tool.towncrier.type]] directory = "feature"