From fc6c9b65f759dcecd72b50a91ba781eeaf412412 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Wed, 16 Feb 2022 22:03:21 +0000 Subject: [PATCH 01/18] Refactor github_pr_comment --- image/Dockerfile | 1 - image/actions.sh | 7 +- image/entrypoints/apply.sh | 5 +- image/entrypoints/plan.sh | 10 +- image/setup.py | 3 +- image/src/github_actions/api.py | 73 +++ image/src/github_actions/cache.py | 34 ++ image/src/github_actions/env.py | 10 +- image/src/github_actions/find_pr.py | 76 +++ image/src/github_actions/inputs.py | 13 +- image/src/github_pr_comment/__init__.py | 0 image/src/github_pr_comment/__main__.py | 247 ++++++++ image/src/github_pr_comment/comment.py | 253 +++++++++ image/tools/github_pr_comment.py | 522 ----------------- tests/github_pr_comment/test_comment.py | 140 +++++ .../github_pr_comment/test_legacy_comment.py | 457 +++++++++++++++ tests/github_pr_comment/test_summary.py | 160 ++++++ tests/test_pr_comment.py | 535 ------------------ 18 files changed, 1464 insertions(+), 1082 deletions(-) create mode 100644 image/src/github_actions/api.py create mode 100644 image/src/github_actions/cache.py create mode 100644 image/src/github_actions/find_pr.py create mode 100644 image/src/github_pr_comment/__init__.py create mode 100644 image/src/github_pr_comment/__main__.py create mode 100644 image/src/github_pr_comment/comment.py delete mode 100755 image/tools/github_pr_comment.py create mode 100644 tests/github_pr_comment/test_comment.py create mode 100644 tests/github_pr_comment/test_legacy_comment.py create mode 100644 tests/github_pr_comment/test_summary.py delete mode 100644 tests/test_pr_comment.py diff --git a/image/Dockerfile b/image/Dockerfile index dd13ed0c..d2a9996a 100644 --- a/image/Dockerfile +++ b/image/Dockerfile @@ -12,7 +12,6 @@ COPY actions.sh /usr/local/actions.sh COPY workflow_commands.sh /usr/local/workflow_commands.sh COPY tools/convert_validate_report.py /usr/local/bin/convert_validate_report -COPY tools/github_pr_comment.py /usr/local/bin/github_pr_comment COPY tools/convert_output.py /usr/local/bin/convert_output COPY tools/plan_cmp.py /usr/local/bin/plan_cmp COPY tools/convert_version.py /usr/local/bin/convert_version diff --git a/image/actions.sh b/image/actions.sh index c0dc32af..ea850d92 100644 --- a/image/actions.sh +++ b/image/actions.sh @@ -96,6 +96,7 @@ function setup() { if [[ "$TERRAFORM_BACKEND_TYPE" != "" ]]; then echo "Detected $TERRAFORM_BACKEND_TYPE backend" fi + export TERRAFORM_BACKEND_TYPE end_group @@ -326,10 +327,8 @@ function output() { function update_status() { local status="$1" - if ! STATUS="$status" github_pr_comment status 2>"$STEP_TMP_DIR/github_pr_comment.stderr"; then - debug_file "$STEP_TMP_DIR/github_pr_comment.stderr" - else - debug_file "$STEP_TMP_DIR/github_pr_comment.stderr" + if ! STATUS="$status" github_pr_comment status; then + echo fi } diff --git a/image/entrypoints/apply.sh b/image/entrypoints/apply.sh index 88bf6fb8..e5f08704 100755 --- a/image/entrypoints/apply.sh +++ b/image/entrypoints/apply.sh @@ -110,16 +110,13 @@ else exit 1 fi - if ! github_pr_comment get "$STEP_TMP_DIR/approved-plan.txt" 2>"$STEP_TMP_DIR/github_pr_comment.stderr"; then - debug_file "$STEP_TMP_DIR/github_pr_comment.stderr" + if ! github_pr_comment get "$STEP_TMP_DIR/approved-plan.txt"; then echo "Plan not found on PR" echo "Generate the plan first using the dflook/terraform-plan action. Alternatively set the auto_approve input to 'true'" echo "If dflook/terraform-plan was used with add_github_comment set to changes-only, this may mean the plan has since changed to include changes" set_output failure-reason plan-changed exit 1 - else - debug_file "$STEP_TMP_DIR/github_pr_comment.stderr" fi if plan_cmp "$STEP_TMP_DIR/plan.txt" "$STEP_TMP_DIR/approved-plan.txt"; then diff --git a/image/entrypoints/plan.sh b/image/entrypoints/plan.sh index a9a2b718..c35b6455 100755 --- a/image/entrypoints/plan.sh +++ b/image/entrypoints/plan.sh @@ -38,11 +38,8 @@ if [[ "$GITHUB_EVENT_NAME" == "pull_request" || "$GITHUB_EVENT_NAME" == "issue_c fi if [[ $PLAN_EXIT -eq 1 ]]; then - if ! STATUS=":x: Failed to generate plan in $(job_markdown_ref)" github_pr_comment plan <"$STEP_TMP_DIR/terraform_plan.stderr" 2>"$STEP_TMP_DIR/github_pr_comment.stderr"; then - debug_file "$STEP_TMP_DIR/github_pr_comment.stderr" + if ! STATUS=":x: Failed to generate plan in $(job_markdown_ref)" github_pr_comment plan <"$STEP_TMP_DIR/terraform_plan.stderr"; then exit 1 - else - debug_file "$STEP_TMP_DIR/github_pr_comment.stderr" fi else @@ -53,11 +50,8 @@ if [[ "$GITHUB_EVENT_NAME" == "pull_request" || "$GITHUB_EVENT_NAME" == "issue_c TF_CHANGES=true fi - if ! TF_CHANGES=$TF_CHANGES STATUS=":memo: Plan generated in $(job_markdown_ref)" github_pr_comment plan <"$STEP_TMP_DIR/plan.txt" 2>"$STEP_TMP_DIR/github_pr_comment.stderr"; then - debug_file "$STEP_TMP_DIR/github_pr_comment.stderr" + if ! TF_CHANGES=$TF_CHANGES STATUS=":memo: Plan generated in $(job_markdown_ref)" github_pr_comment plan <"$STEP_TMP_DIR/plan.txt"; then exit 1 - else - debug_file "$STEP_TMP_DIR/github_pr_comment.stderr" fi fi diff --git a/image/setup.py b/image/setup.py index d8ae8272..d810109c 100644 --- a/image/setup.py +++ b/image/setup.py @@ -10,7 +10,8 @@ 'console_scripts': [ 'terraform-backend=terraform_backend.__main__:main', 'terraform-version=terraform_version.__main__:main', - 'terraform-cloud-workspace=terraform_cloud_workspace.__main__:main' + 'terraform-cloud-workspace=terraform_cloud_workspace.__main__:main', + 'github_pr_comment=github_pr_comment.__main__:main' ] }, install_requires=[ diff --git a/image/src/github_actions/api.py b/image/src/github_actions/api.py new file mode 100644 index 00000000..3d99a735 --- /dev/null +++ b/image/src/github_actions/api.py @@ -0,0 +1,73 @@ +import datetime +import sys +from typing import NewType, Iterable, Any + +import requests +from requests import Response + +from github_actions.debug import debug + +GitHubUrl = NewType('GitHubUrl', str) +PrUrl = NewType('PrUrl', GitHubUrl) +IssueUrl = NewType('IssueUrl', GitHubUrl) +CommentUrl = NewType('CommentUrl', GitHubUrl) +CommentReactionUrl = NewType('CommentReactionUrl', GitHubUrl) + + +class GithubApi: + def __init__(self, host: str, token: str): + self._host = host + self._token = token + + self._session = requests.Session() + self._session.headers['authorization'] = f'token {token}' + self._session.headers['user-agent'] = 'terraform-github-actions' + self._session.headers['accept'] = 'application/vnd.github.v3+json' + + def api_request(self, method: str, *args, **kwargs) -> requests.Response: + response = self._session.request(method, *args, **kwargs) + + if 400 <= response.status_code < 500: + debug(str(response.headers)) + + try: + message = response.json()['message'] + + if response.headers['X-RateLimit-Remaining'] == '0': + limit_reset = datetime.datetime.fromtimestamp(int(response.headers['X-RateLimit-Reset'])) + sys.stdout.write(message) + sys.stdout.write(f' Try again when the rate limit resets at {limit_reset} UTC.\n') + sys.exit(1) + + if message != 'Resource not accessible by integration': + sys.stdout.write(message) + sys.stdout.write('\n') + debug(response.content.decode()) + + except Exception: + sys.stdout.write(response.content.decode()) + sys.stdout.write('\n') + raise + + return response + + def get(self, path: str, **kwargs: Any) -> Response: + return self.api_request('GET', path, **kwargs) + + def post(self, path: str, **kwargs: Any) -> Response: + return self.api_request('POST', path, **kwargs) + + def patch(self, path: str, **kwargs: Any) -> Response: + return self.api_request('PATCH', path, **kwargs) + + def paged_get(self, url: GitHubUrl, *args, **kwargs) -> Iterable[dict[str, Any]]: + while True: + response = self.api_request('GET', url, *args, **kwargs) + response.raise_for_status() + + yield from response.json() + + if 'next' in response.links: + url = response.links['next']['url'] + else: + return diff --git a/image/src/github_actions/cache.py b/image/src/github_actions/cache.py new file mode 100644 index 00000000..c27b6290 --- /dev/null +++ b/image/src/github_actions/cache.py @@ -0,0 +1,34 @@ +import os +from pathlib import Path + +from github_actions.debug import debug + + +class ActionsCache: + + def __init__(self, cache_dir: Path, label: str=None): + self._cache_dir = cache_dir + self._label = label or self._cache_dir + + def __setitem__(self, key, value): + if value is None: + debug(f'Cache value for {key} should not be set to {value}') + return + + path = os.path.join(self._cache_dir, key) + + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(os.path.join(self._cache_dir, key), 'w') as f: + f.write(value) + debug(f'Wrote {key} to {self._label}') + + def __getitem__(self, key): + if os.path.isfile(os.path.join(self._cache_dir, key)): + with open(os.path.join(self._cache_dir, key)) as f: + debug(f'Read {key} from {self._label}') + return f.read() + + raise IndexError(key) + + def __contains__(self, key): + return os.path.isfile(os.path.join(self._cache_dir, key)) diff --git a/image/src/github_actions/env.py b/image/src/github_actions/env.py index a4d6cb13..f1ec3bc9 100644 --- a/image/src/github_actions/env.py +++ b/image/src/github_actions/env.py @@ -15,5 +15,13 @@ class ActionsEnv(TypedDict): class GithubEnv(TypedDict): - """Environment variables set by github actions.""" + """Environment variables that are set by the actions runner.""" + GITHUB_API_URL: str + GITHUB_TOKEN: str + GITHUB_EVENT_PATH: str + GITHUB_EVENT_NAME: str + GITHUB_REPOSITORY: str + GITHUB_SHA: str + GITHUB_REF_TYPE: str + GITHUB_REF: str GITHUB_WORKSPACE: str diff --git a/image/src/github_actions/find_pr.py b/image/src/github_actions/find_pr.py new file mode 100644 index 00000000..0fefb4bc --- /dev/null +++ b/image/src/github_actions/find_pr.py @@ -0,0 +1,76 @@ +import json +import os +import re +from typing import Optional, Any, cast, Iterable + +from github_actions.api import PrUrl, GithubApi +from github_actions.debug import debug +from github_actions.env import GithubEnv + + +class WorkflowException(Exception): + """An exception that should result in an error in the workflow log""" + + +def find_pr(github: GithubApi, actions_env: GithubEnv) -> PrUrl: + """ + Find the pull request this event is related to + + >>> find_pr() + 'https://api.github.com/repos/dflook/terraform-github-actions/pulls/8' + + """ + + event: Optional[dict[str, Any]] + + if os.path.isfile(actions_env['GITHUB_EVENT_PATH']): + with open(actions_env['GITHUB_EVENT_PATH']) as f: + event = json.load(f) + else: + debug('Event payload is not available') + event = None + + event_type = actions_env['GITHUB_EVENT_NAME'] + + if event_type in ['pull_request', 'pull_request_review_comment', 'pull_request_target', 'pull_request_review', 'issue_comment']: + + if event is not None: + # Pull pr url from event payload + + if event_type in ['pull_request', 'pull_request_review_comment', 'pull_request_target', 'pull_request_review']: + return cast(PrUrl, event['pull_request']['url']) + + if event_type == 'issue_comment': + + if 'pull_request' in event['issue']: + return cast(PrUrl, event['issue']['pull_request']['url']) + else: + raise WorkflowException('This comment is not for a PR. Add a filter of `if: github.event.issue.pull_request`') + + else: + # Event payload is not available + + if actions_env.get('GITHUB_REF_TYPE') == 'branch': + if match := re.match(r'refs/pull/(\d+)/', actions_env.get('GITHUB_REF', '')): + return cast(PrUrl, f'{actions_env["GITHUB_API_URL"]}/repos/{actions_env["GITHUB_REPOSITORY"]}/pulls/{match.group(1)}') + + raise WorkflowException(f'Event payload is not available at the GITHUB_EVENT_PATH {actions_env["GITHUB_EVENT_PATH"]!r}. ' + + f'This is required when run by {event_type} events. The environment has not been setup properly by the actions runner. ' + + 'This can happen when the runner is running in a container') + + elif event_type == 'push': + repo = actions_env['GITHUB_REPOSITORY'] + commit = actions_env['GITHUB_SHA'] + + def prs() -> Iterable[dict[str, Any]]: + url = cast(PrUrl, f'{actions_env["GITHUB_API_URL"]}/repos/{repo}/pulls') + yield from github.paged_get(url, params={'state': 'all'}) + + for pr in prs(): + if pr['merge_commit_sha'] == commit: + return cast(PrUrl, pr['url']) + + raise WorkflowException(f'No PR found in {repo} for commit {commit} (was it pushed directly to the target branch?)') + + else: + raise WorkflowException(f"The {event_type} event doesn\'t relate to a Pull Request.") diff --git a/image/src/github_actions/inputs.py b/image/src/github_actions/inputs.py index 627c63c1..231d4fbc 100644 --- a/image/src/github_actions/inputs.py +++ b/image/src/github_actions/inputs.py @@ -23,19 +23,20 @@ class PlanInputs(InitInputs): INPUT_PARALLELISM: str -class Plan(PlanInputs): - """Input variables for the plan action""" +class PlanPrInputs(PlanInputs): + """Common input variables for actions that use a PR comment""" INPUT_LABEL: str INPUT_TARGET: str INPUT_REPLACE: str + + +class Plan(PlanPrInputs): + """Input variables for the plan action""" INPUT_ADD_GITHUB_COMMENT: str -class Apply(InitInputs): +class Apply(PlanPrInputs): """Input variables for the terraform-apply action""" - INPUT_LABEL: str - INPUT_TARGET: str - INPUT_REPLACE: str INPUT_AUTO_APPROVE: str diff --git a/image/src/github_pr_comment/__init__.py b/image/src/github_pr_comment/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/image/src/github_pr_comment/__main__.py b/image/src/github_pr_comment/__main__.py new file mode 100644 index 00000000..74719391 --- /dev/null +++ b/image/src/github_pr_comment/__main__.py @@ -0,0 +1,247 @@ +import hashlib +import json +import os +import sys +from typing import (NewType, Optional, cast) + +from github_actions.api import GithubApi, IssueUrl, PrUrl +from github_actions.cache import ActionsCache +from github_actions.debug import debug +from github_actions.env import GithubEnv +from github_actions.find_pr import find_pr, WorkflowException +from github_actions.inputs import PlanPrInputs +from github_pr_comment.comment import find_comment, TerraformComment, update_comment + +Plan = NewType('Plan', str) +Status = NewType('Status', str) + +job_cache = ActionsCache(os.environ.get('JOB_TMP_DIR', '.'), 'job_cache') +step_cache = ActionsCache(os.environ.get('STEP_TMP_DIR', '.'), 'step_cache') + +env = cast(GithubEnv, os.environ) + +github = GithubApi(env.get('GITHUB_API_URL', 'https://api.github.com'), env.get('GITHUB_TOKEN')) + + +def _mask_backend_config(action_inputs: PlanPrInputs) -> Optional[str]: + bad_words = [ + 'token', + 'password', + 'sas_token', + 'access_key', + 'secret_key', + 'client_secret', + 'access_token', + 'http_auth', + 'secret_id', + 'encryption_key', + 'key_material', + 'security_token', + 'conn_str', + 'sse_customer_key', + 'application_credential_secret' + ] + + clean = [] + + for field in action_inputs.get('INPUT_BACKEND_CONFIG', '').split(','): + if not field: + continue + + if not any(bad_word in field for bad_word in bad_words): + clean.append(field) + + return ','.join(clean) + + +def format_classic_description(action_inputs: PlanPrInputs) -> str: + if action_inputs['INPUT_LABEL']: + return f'Terraform plan for __{action_inputs["INPUT_LABEL"]}__' + + label = f'Terraform plan in __{action_inputs["INPUT_PATH"]}__' + + if action_inputs["INPUT_WORKSPACE"] != 'default': + label += f' in the __{action_inputs["INPUT_WORKSPACE"]}__ workspace' + + if action_inputs["INPUT_TARGET"]: + label += '\nTargeting resources: ' + label += ', '.join(f'`{res.strip()}`' for res in action_inputs['INPUT_TARGET'].splitlines()) + + if action_inputs["INPUT_REPLACE"]: + label += '\nReplacing resources: ' + label += ', '.join(f'`{res.strip()}`' for res in action_inputs['INPUT_REPLACE'].splitlines()) + + if backend_config := _mask_backend_config(action_inputs): + label += f'\nWith backend config: `{backend_config}`' + + if action_inputs["INPUT_BACKEND_CONFIG_FILE"]: + label += f'\nWith backend config files: `{action_inputs["INPUT_BACKEND_CONFIG_FILE"]}`' + + if action_inputs["INPUT_VAR"]: + label += f'\nWith vars: `{action_inputs["INPUT_VAR"]}`' + + if action_inputs["INPUT_VAR_FILE"]: + label += f'\nWith var files: `{action_inputs["INPUT_VAR_FILE"]}`' + + if action_inputs["INPUT_VARIABLES"]: + stripped_vars = action_inputs["INPUT_VARIABLES"].strip() + if '\n' in stripped_vars: + label += f'''
With variables + +```hcl +{stripped_vars} +``` +
+''' + else: + label += f'\nWith variables: `{stripped_vars}`' + + return label + + +def create_summary(plan: Plan) -> Optional[str]: + summary = None + + for line in plan.splitlines(): + if line.startswith('No changes') or line.startswith('Error'): + return line + + if line.startswith('Plan:'): + summary = line + + if line.startswith('Changes to Outputs'): + if summary: + return summary + ' Changes to Outputs.' + else: + return 'Changes to Outputs' + + return summary + + +def current_user(actions_env: GithubEnv) -> str: + token_hash = hashlib.sha256(actions_env['GITHUB_TOKEN'].encode()).hexdigest() + cache_key = f'token-cache/{token_hash}' + + if cache_key in job_cache: + username = job_cache[cache_key] + else: + response = github.get(f'{actions_env["GITHUB_API_URL"]}/user') + if response.status_code != 403: + user = response.json() + debug(json.dumps(user)) + + username = user['login'] + else: + # Assume this is the github actions app token + username = 'github-actions[bot]' + + job_cache[cache_key] = username + + return username + + +def get_issue_url(pr_url: str) -> IssueUrl: + pr_hash = hashlib.sha256(pr_url.encode()).hexdigest() + cache_key = f'issue-href-cache/{pr_hash}' + + if cache_key in job_cache: + issue_url = job_cache[cache_key] + else: + response = github.get(pr_url) + response.raise_for_status() + issue_url = response.json()['_links']['issue']['href'] + '/comments' + + job_cache[cache_key] = issue_url + + return cast(IssueUrl, issue_url) + + +def get_pr() -> PrUrl: + if 'pr_url' in step_cache: + pr_url = step_cache['pr_url'] + else: + try: + pr_url = find_pr(github, env) + step_cache['pr_url'] = pr_url + except WorkflowException as e: + sys.stderr.write('\n' + str(e) + '\n') + sys.exit(1) + + return cast(PrUrl, pr_url) + + +def get_comment(action_inputs: PlanPrInputs) -> TerraformComment: + pr_url = get_pr() + issue_url = get_issue_url(pr_url) + username = current_user(env) + + legacy_description = format_classic_description(action_inputs) + + headers = { + 'workspace': os.environ.get('INPUT_WORKSPACE', 'default'), + 'backend': hashlib.sha256(legacy_description.encode()).hexdigest() + } + + if backend_type := os.environ.get('TERRAFORM_BACKEND_TYPE'): + headers['backend_type'] = backend_type + + if label := os.environ.get('INPUT_LABEL'): + headers['label'] = hashlib.sha256(label.encode()).hexdigest() + + return find_comment(github, issue_url, username, headers, legacy_description) + + +def main() -> int: + if len(sys.argv) < 2: + sys.stderr.write(f'''Usage: + STATUS="" {sys.argv[0]} plan Optional[CommentUrl]: + return self._comment_url + + @comment_url.setter + def comment_url(self, comment_url: CommentUrl) -> None: + if self._comment_url is not None: + raise Exception('Can only set url for comments that don\'t exist yet') + self._comment_url = comment_url + + @property + def issue_url(self) -> IssueUrl: + return self._issue_url + + @property + def headers(self) -> dict[str, str]: + return self._headers + + @property + def description(self) -> str: + return self._description + + @property + def summary(self) -> str: + return self._summary + + @property + def body(self) -> str: + return self._body + + @property + def status(self) -> str: + return self._status + + +def _format_comment_header(**kwargs) -> str: + return f'' + +def _parse_comment_header(comment_header: Optional[str]) -> dict[str, str]: + if comment_header is None: + return {} + + if header := re.match(r'^', comment_header): + try: + return json.loads(header['args']) + except JSONDecodeError: + return {} + + return {} + + +def _from_api_payload(comment: dict[str, Any]) -> Optional[TerraformComment]: + match = re.match(rf''' + (?P\n)? + (?P.*) + \s* + (?:(?P.*?)\s*)? + ```(?:hcl)? + (?P.*) + ```\s* + + (?P.*) + ''', + comment['body'], + re.VERBOSE | re.DOTALL + ) + + if not match: + return None + + return TerraformComment( + issue_url=comment['issue_url'], + comment_url=comment['url'], + headers=_parse_comment_header(match.group('headers')), + description=match.group('description').strip(), + summary=match.group('summary').strip(), + body=match.group('body').strip(), + status=match.group('status').strip() + ) + + +def _to_api_payload(comment: TerraformComment) -> str: + details_open = False + hcl_highlighting = False + + if comment.body.startswith('Error'): + details_open = True + elif 'Plan:' in comment.body: + hcl_highlighting = True + num_lines = len(comment.body.splitlines()) + if num_lines < collapse_threshold: + details_open = True + + if comment.summary is None: + details_open = True + + header = _format_comment_header(**comment.headers) + + body = f'''{header} +{comment.description} + +{f'{comment.summary}' if comment.summary is not None else ''} + +```{'hcl' if hcl_highlighting else ''} +{comment.body} +``` + +''' + + if comment.status: + body += '\n' + comment.status + + return body + + +def find_comment(github: GithubApi, issue_url: IssueUrl, username: str, headers: dict[str, str], legacy_description: str) -> TerraformComment: + """ + Find a github comment that matches the given headers + + If no comment is found with the specified headers, tries to find a comment that matches the specified description instead. + This is in case the comment was made with an earlier version, where comments were matched by description only. + + If not existing comment is found a new TerraformComment object is returned which represents a PR comment yet to be created. + + :param github: The github api object to make requests with + :param issue_url: The issue to find the comment in + :param username: The user who made the comment + :param headers: The headers that must be present on the comment + :param legacy_description: The description that must be present on the comment, if not headers are found. + """ + + backup_comment = None + + for comment_payload in github.paged_get(issue_url): + if comment_payload['user']['login'] != username: + continue + + debug(json.dumps(comment_payload)) + + if comment := _from_api_payload(comment_payload): + + if comment.headers == headers: + debug('Found comment that matches headers') + return comment + + debug(f"Didn't match comment with {comment.headers=}") + + if comment.description == legacy_description: + backup_comment = comment + + debug(f"Didn't match comment with {comment.description=}") + + if backup_comment is not None: + debug('Found comment matching legacy description') + return backup_comment + + debug('No matching comment exists') + return TerraformComment( + issue_url=issue_url, + comment_url=None, + headers=headers, + description='', + summary='', + body='', + status='' + ) + + +def update_comment( + github: GithubApi, + comment: TerraformComment, + *, + headers: dict[str, str] = None, + description: str = None, + summary: str = None, + body: str = None, + status: str = None +) -> TerraformComment: + + new_comment = TerraformComment( + issue_url=comment.issue_url, + comment_url=comment.comment_url, + headers=headers if headers is not None else comment.headers, + description=description if description is not None else comment.description, + summary=summary if summary is not None else comment.summary, + body=body if body is not None else comment.body, + status=status if status is not None else comment.status + ) + + if comment.comment_url is not None: + response = github.patch(comment.comment_url, json={'body': _to_api_payload(new_comment)}) + response.raise_for_status() + else: + response = github.post(comment.issue_url, json={'body': _to_api_payload(new_comment)}) + response.raise_for_status() + new_comment.url = response.json()['url'] + + return new_comment diff --git a/image/tools/github_pr_comment.py b/image/tools/github_pr_comment.py deleted file mode 100755 index 52906bbb..00000000 --- a/image/tools/github_pr_comment.py +++ /dev/null @@ -1,522 +0,0 @@ -#!/usr/bin/python3 - -import datetime -import hashlib -import json -import os -import re -import sys -from typing import (Any, Dict, Iterable, NewType, Optional, Tuple, TypedDict, - cast) - -import requests - -GitHubUrl = NewType('GitHubUrl', str) -PrUrl = NewType('PrUrl', GitHubUrl) -IssueUrl = NewType('IssueUrl', GitHubUrl) -CommentUrl = NewType('CommentUrl', GitHubUrl) -Plan = NewType('Plan', str) -Status = NewType('Status', str) - - -class GitHubActionsEnv(TypedDict): - """ - Environment variables that are set by the actions runner - """ - GITHUB_API_URL: str - GITHUB_TOKEN: str - GITHUB_EVENT_PATH: str - GITHUB_EVENT_NAME: str - GITHUB_REPOSITORY: str - GITHUB_SHA: str - GITHUB_REF_TYPE: str - GITHUB_REF: str - - -job_tmp_dir = os.environ.get('JOB_TMP_DIR', '.') -step_tmp_dir = os.environ.get('STEP_TMP_DIR', '.') - -env = cast(GitHubActionsEnv, os.environ) - - -def github_session(github_env: GitHubActionsEnv) -> requests.Session: - """ - A request session that is configured for the github API - """ - session = requests.Session() - session.headers['authorization'] = f'token {github_env["GITHUB_TOKEN"]}' - session.headers['user-agent'] = 'terraform-github-actions' - session.headers['accept'] = 'application/vnd.github.v3+json' - return session - - -github = github_session(env) - - -class ActionInputs(TypedDict): - """ - Actions input environment variables that are set by the runner - """ - INPUT_BACKEND_CONFIG: str - INPUT_BACKEND_CONFIG_FILE: str - INPUT_VARIABLES: str - INPUT_VAR: str - INPUT_VAR_FILE: str - INPUT_PATH: str - INPUT_WORKSPACE: str - INPUT_LABEL: str - INPUT_ADD_GITHUB_COMMENT: str - INPUT_TARGET: str - INPUT_REPLACE: str - -class WorkflowException(Exception): - """An exception that should result in an error in the workflow log""" - -def plan_identifier(action_inputs: ActionInputs) -> str: - def mask_backend_config() -> Optional[str]: - - bad_words = [ - 'token', - 'password', - 'sas_token', - 'access_key', - 'secret_key', - 'client_secret', - 'access_token', - 'http_auth', - 'secret_id', - 'encryption_key', - 'key_material', - 'security_token', - 'conn_str', - 'sse_customer_key', - 'application_credential_secret' - ] - - def has_bad_word(s: str) -> bool: - for bad_word in bad_words: - if bad_word in s: - return True - return False - - clean = [] - - for field in action_inputs.get('INPUT_BACKEND_CONFIG', '').split(','): - if not field: - continue - - if not has_bad_word(field): - clean.append(field) - - return ','.join(clean) - - if action_inputs['INPUT_LABEL']: - return f'Terraform plan for __{action_inputs["INPUT_LABEL"]}__' - - label = f'Terraform plan in __{action_inputs["INPUT_PATH"]}__' - - if action_inputs["INPUT_WORKSPACE"] != 'default': - label += f' in the __{action_inputs["INPUT_WORKSPACE"]}__ workspace' - - if action_inputs["INPUT_TARGET"]: - label += '\nTargeting resources: ' - label += ', '.join(f'`{res.strip()}`' for res in action_inputs['INPUT_TARGET'].splitlines()) - - if action_inputs["INPUT_REPLACE"]: - label += '\nReplacing resources: ' - label += ', '.join(f'`{res.strip()}`' for res in action_inputs['INPUT_REPLACE'].splitlines()) - - backend_config = mask_backend_config() - if backend_config: - label += f'\nWith backend config: `{backend_config}`' - - if action_inputs["INPUT_BACKEND_CONFIG_FILE"]: - label += f'\nWith backend config files: `{action_inputs["INPUT_BACKEND_CONFIG_FILE"]}`' - - if action_inputs["INPUT_VAR"]: - label += f'\nWith vars: `{action_inputs["INPUT_VAR"]}`' - - if action_inputs["INPUT_VAR_FILE"]: - label += f'\nWith var files: `{action_inputs["INPUT_VAR_FILE"]}`' - - if action_inputs["INPUT_VARIABLES"]: - stripped_vars = action_inputs["INPUT_VARIABLES"].strip() - if '\n' in stripped_vars: - label += f'''
With variables - -```hcl -{stripped_vars} -``` -
-''' - else: - label += f'\nWith variables: `{stripped_vars}`' - - return label - - -def github_api_request(method: str, *args, **kwargs) -> requests.Response: - response = github.request(method, *args, **kwargs) - - if 400 <= response.status_code < 500: - debug(str(response.headers)) - - try: - message = response.json()['message'] - - if response.headers['X-RateLimit-Remaining'] == '0': - limit_reset = datetime.datetime.fromtimestamp(int(response.headers['X-RateLimit-Reset'])) - sys.stdout.write(message) - sys.stdout.write(f' Try again when the rate limit resets at {limit_reset} UTC.\n') - sys.exit(1) - - if message != 'Resource not accessible by integration': - sys.stdout.write(message) - sys.stdout.write('\n') - debug(response.content.decode()) - - except Exception: - sys.stdout.write(response.content.decode()) - sys.stdout.write('\n') - raise - - return response - - -def debug(msg: str) -> None: - sys.stderr.write(msg) - sys.stderr.write('\n') - - -def paginate(url: GitHubUrl, *args, **kwargs) -> Iterable[Dict[str, Any]]: - while True: - response = github_api_request('get', url, *args, **kwargs) - response.raise_for_status() - - yield from response.json() - - if 'next' in response.links: - url = response.links['next']['url'] - else: - return - - -def find_pr(actions_env: GitHubActionsEnv) -> PrUrl: - """ - Find the pull request this event is related to - - >>> find_pr() - 'https://api.github.com/repos/dflook/terraform-github-actions/pulls/8' - - """ - - event: Optional[Dict[str, Any]] - - if os.path.isfile(actions_env['GITHUB_EVENT_PATH']): - with open(actions_env['GITHUB_EVENT_PATH']) as f: - event = json.load(f) - else: - debug('Event payload is not available') - event = None - - event_type = actions_env['GITHUB_EVENT_NAME'] - - if event_type in ['pull_request', 'pull_request_review_comment', 'pull_request_target', 'pull_request_review', 'issue_comment']: - - if event is not None: - # Pull pr url from event payload - - if event_type in ['pull_request', 'pull_request_review_comment', 'pull_request_target', 'pull_request_review']: - return cast(PrUrl, event['pull_request']['url']) - - if event_type == 'issue_comment': - - if 'pull_request' in event['issue']: - return cast(PrUrl, event['issue']['pull_request']['url']) - else: - raise WorkflowException('This comment is not for a PR. Add a filter of `if: github.event.issue.pull_request`') - - else: - # Event payload is not available - - if actions_env.get('GITHUB_REF_TYPE') == 'branch': - if match := re.match(r'refs/pull/(\d+)/', actions_env.get('GITHUB_REF', '')): - return cast(PrUrl, f'{actions_env["GITHUB_API_URL"]}/repos/{actions_env["GITHUB_REPOSITORY"]}/pulls/{match.group(1)}') - - raise WorkflowException(f'Event payload is not available at the GITHUB_EVENT_PATH {actions_env["GITHUB_EVENT_PATH"]!r}. ' + - f'This is required when run by {event_type} events. The environment has not been setup properly by the actions runner. ' + - 'This can happen when the runner is running in a container') - - elif event_type == 'push': - repo = actions_env['GITHUB_REPOSITORY'] - commit = actions_env['GITHUB_SHA'] - - def prs() -> Iterable[Dict[str, Any]]: - url = f'{actions_env["GITHUB_API_URL"]}/repos/{repo}/pulls' - yield from paginate(cast(PrUrl, url), params={'state': 'all'}) - - for pr in prs(): - if pr['merge_commit_sha'] == commit: - return cast(PrUrl, pr['url']) - - raise WorkflowException(f'No PR found in {repo} for commit {commit} (was it pushed directly to the target branch?)') - - else: - raise WorkflowException(f"The {event_type} event doesn\'t relate to a Pull Request.") - - -def current_user(actions_env: GitHubActionsEnv) -> str: - token_hash = hashlib.sha256(actions_env['GITHUB_TOKEN'].encode()).hexdigest() - - try: - with open(os.path.join(job_tmp_dir, 'token-cache', token_hash)) as f: - username = f.read() - debug(f'GITHUB_TOKEN username from token-cache: {username}') - return username - except Exception as e: - debug(str(e)) - - response = github_api_request('get', f'{actions_env["GITHUB_API_URL"]}/user') - if response.status_code != 403: - user = response.json() - debug(json.dumps(user)) - - username = user['login'] - else: - # Assume this is the github actions app token - username = 'github-actions[bot]' - - try: - os.makedirs(os.path.join(job_tmp_dir, 'token-cache'), exist_ok=True) - with open(os.path.join(job_tmp_dir, 'token-cache', token_hash), 'w') as f: - f.write(username) - except Exception as e: - debug(str(e)) - - debug(f'discovered GITHUB_TOKEN username: {username}') - return username - - -def create_summary(plan) -> Optional[str]: - summary = None - - for line in plan.splitlines(): - if line.startswith('No changes') or line.startswith('Error'): - return line - - if line.startswith('Plan:'): - summary = line - - if line.startswith('Changes to Outputs'): - if summary: - return summary + ' Changes to Outputs.' - else: - return 'Changes to Outputs' - - return summary - - -def format_body(action_inputs: ActionInputs, plan: Plan, status: Status, collapse_threshold: int) -> str: - - details_open = '' - highlighting = '' - - summary_line = create_summary(plan) - - if plan.startswith('Error'): - details_open = ' open' - elif 'Plan:' in plan: - highlighting = 'hcl' - num_lines = len(plan.splitlines()) - if num_lines < collapse_threshold: - details_open = ' open' - - if summary_line is None: - details_open = ' open' - - body = f'''{plan_identifier(action_inputs)} - -{ f'{summary_line}' if summary_line is not None else '' } - -```{highlighting} -{plan} -``` - -''' - - if status: - body += '\n' + status - - return body - - -def update_comment(issue_url: IssueUrl, - comment_url: Optional[CommentUrl], - body: str, - only_if_exists: bool = False) -> Optional[CommentUrl]: - """ - Update (or create) a comment - - :param issue_url: The url of the issue to create or update the comment in - :param comment_url: The url of the comment to update - :param body: The new comment body - :param only_if_exists: Only update an existing comment - don't create it - """ - - if comment_url is None: - if only_if_exists: - debug('Comment doesn\'t already exist - not creating it') - return None - # Create a new comment - debug('Creating comment') - response = github_api_request('post', issue_url, json={'body': body}) - else: - # Update existing comment - debug('Updating existing comment') - response = github_api_request('patch', comment_url, json={'body': body}) - - debug(body) - debug(response.content.decode()) - response.raise_for_status() - return cast(CommentUrl, response.json()['url']) - - -def find_issue_url(pr_url: str) -> IssueUrl: - pr_hash = hashlib.sha256(pr_url.encode()).hexdigest() - - try: - with open(os.path.join(job_tmp_dir, 'issue-href-cache', pr_hash)) as f: - issue_url = f.read() - debug(f'issue_url from issue-href-cache: {issue_url}') - return cast(IssueUrl, issue_url) - except Exception as e: - debug(str(e)) - - response = github_api_request('get', pr_url) - response.raise_for_status() - - issue_url = cast(IssueUrl, response.json()['_links']['issue']['href'] + '/comments') - - try: - os.makedirs(os.path.join(job_tmp_dir, 'issue-href-cache'), exist_ok=True) - with open(os.path.join(job_tmp_dir, 'issue-href-cache', pr_hash), 'w') as f: - f.write(issue_url) - except Exception as e: - debug(str(e)) - - debug(f'discovered issue_url: {issue_url}') - return cast(IssueUrl, issue_url) - - -def find_comment(issue_url: IssueUrl, username: str, action_inputs: ActionInputs) -> Tuple[Optional[CommentUrl], Optional[Plan]]: - debug('Looking for an existing comment:') - - plan_id = plan_identifier(action_inputs) - - for comment in paginate(issue_url): - debug(json.dumps(comment)) - if comment['user']['login'] == username: - match = re.match(rf'{re.escape(plan_id)}.*```(?:hcl)?(.*?)```.*', comment['body'], re.DOTALL) - - if match: - return comment['url'], cast(Plan, match.group(1).strip()) - - return None, None - -def read_step_cache() -> Dict[str, str]: - try: - with open(os.path.join(step_tmp_dir, 'github_pr_comment.cache')) as f: - debug('step cache loaded') - return json.load(f) - except Exception as e: - debug(str(e)) - return {} - -def save_step_cache(**kwargs) -> None: - try: - with open(os.path.join(step_tmp_dir, 'github_pr_comment.cache'), 'w') as f: - json.dump(kwargs, f) - debug('step cache saved') - except Exception as e: - debug(str(e)) - -def main() -> None: - if len(sys.argv) < 2: - sys.stdout.write(f'''Usage: - STATUS="" {sys.argv[0]} plan plan.txt -''') - - debug(repr(sys.argv)) - - action_inputs = cast(ActionInputs, os.environ) - - try: - collapse_threshold = int(os.environ['TF_PLAN_COLLAPSE_LENGTH']) - except (ValueError, KeyError): - collapse_threshold = 10 - - step_cache = read_step_cache() - - if step_cache.get('pr_url') is not None: - pr_url = step_cache['pr_url'] - debug(f'pr_url from step cache: {pr_url}') - else: - try: - pr_url = find_pr(env) - except WorkflowException as e: - sys.stdout.write('\n' + str(e) + '\n') - sys.exit(1) - debug(f'discovered pr_url: {pr_url}') - - if step_cache.get('pr_url') == pr_url and step_cache.get('issue_url') is not None: - issue_url = step_cache['issue_url'] - debug(f'issue_url from step cache: {issue_url}') - else: - issue_url = find_issue_url(pr_url) - - # Username is cached in the job tmp dir - username = current_user(env) - - if step_cache.get('comment_url') is not None and step_cache.get('plan') is not None: - comment_url = step_cache['comment_url'] - plan = step_cache['plan'] - debug(f'comment_url from step cache: {comment_url}') - debug(f'plan from step cache: {plan}') - else: - comment_url, plan = find_comment(issue_url, username, action_inputs) - debug(f'discovered comment_url: {comment_url}') - debug(f'discovered plan: {plan}') - - status = cast(Status, os.environ.get('STATUS', '')) - - only_if_exists = False - - if sys.argv[1] == 'plan': - plan = cast(Plan, sys.stdin.read().strip()) - - if action_inputs['INPUT_ADD_GITHUB_COMMENT'] == 'changes-only' and os.environ.get('TF_CHANGES', 'true') == 'false': - only_if_exists = True - - body = format_body(action_inputs, plan, status, collapse_threshold) - comment_url = update_comment(issue_url, comment_url, body, only_if_exists) - - elif sys.argv[1] == 'status': - if plan is None: - sys.exit(1) - else: - body = format_body(action_inputs, plan, status, collapse_threshold) - comment_url = update_comment(issue_url, comment_url, body, only_if_exists) - - elif sys.argv[1] == 'get': - if plan is None: - sys.exit(1) - - with open(sys.argv[2], 'w') as f: - f.write(plan) - - save_step_cache(pr_url=pr_url, issue_url=issue_url, comment_url=comment_url, plan=plan) - -if __name__ == '__main__': - main() diff --git a/tests/github_pr_comment/test_comment.py b/tests/github_pr_comment/test_comment.py new file mode 100644 index 00000000..61823233 --- /dev/null +++ b/tests/github_pr_comment/test_comment.py @@ -0,0 +1,140 @@ +import random +import string + +from github_pr_comment.comment import _format_comment_header, _parse_comment_header, TerraformComment, _to_api_payload, _from_api_payload + + +def test_comment_header(): + header_args = { + 'workspace_name': 'default', + 'backend_config': 'backend_config1' + } + + expected_header = '' + actual_header = _format_comment_header(**header_args) + assert actual_header == expected_header + + assert _parse_comment_header(expected_header) == header_args + + wonky_header = '' + assert _parse_comment_header(wonky_header) == header_args + + +def test_no_headers(): + issue_url = ''.join(random.choice(string.ascii_letters) for _ in range(10)) + comment_url = ''.join(random.choice(string.ascii_letters) for _ in range(10)) + status = 'Testing' + description = 'Hello, this is a description' + summary = 'Some changes' + body = '''An execution plan has been generated and is shown below. +... +Plan: 1 to add, 0 to change, 0 to destroy. +''' + + expected = TerraformComment( + issue_url=issue_url, + comment_url=comment_url, + status=status, + headers={}, + description=description, + summary=summary, + body=body + ) + + assert _from_api_payload({ + 'body': _to_api_payload(expected), + 'url': comment_url, + 'issue_url': issue_url + }) == expected + + +def test_headers(): + issue_url = ''.join(random.choice(string.ascii_letters) for _ in range(10)) + comment_url = ''.join(random.choice(string.ascii_letters) for _ in range(10)) + status = 'Testing' + description = 'Hello, this is a description' + summary = 'Some changes' + body = '''An execution plan has been generated and is shown below. +... +Plan: 1 to add, 0 to change, 0 to destroy. +''' + headers = { + 'hello': 'first_header_value', + 'there': 'second_header_value' + } + + expected = TerraformComment( + issue_url=issue_url, + comment_url=comment_url, + status=status, + headers=headers, + description=description, + summary=summary, + body=body + ) + + assert _from_api_payload({ + 'body': _to_api_payload(expected), + 'url': comment_url, + 'issue_url': issue_url + }) == expected + + +def test_bad_description(): + issue_url = ''.join(random.choice(string.ascii_letters) for _ in range(10)) + comment_url = ''.join(random.choice(string.ascii_letters) for _ in range(10)) + status = 'Testing' + summary = 'Some changes' + body = '''blah blah body''' + description = 'crap -->\nqweqwesomething something
' + + headers = { + 'hello': 'first_header_value', + 'there': 'second_header_value' + } + + expected = TerraformComment( + issue_url=issue_url, + comment_url=comment_url, + status=status, + headers=headers, + description=description, + summary=summary, + body=body + ) + + assert _from_api_payload({ + 'body': _to_api_payload(expected), + 'url': comment_url, + 'issue_url': issue_url + }) == expected + + +def test_bad_body(): + issue_url = ''.join(random.choice(string.ascii_letters) for _ in range(10)) + comment_url = ''.join(random.choice(string.ascii_letters) for _ in range(10)) + status = 'Testing' + summary = 'Some changes' + description = '''blah blah description''' + body = 'qweqwe
something something ```' + + headers = { + 'hello': 'first_header_value', + 'there': 'second_header_value' + } + + expected = TerraformComment( + issue_url=issue_url, + comment_url=comment_url, + status=status, + headers=headers, + description=description, + summary=summary, + body=body + ) + + assert _from_api_payload({ + 'body': _to_api_payload(expected), + 'url': comment_url, + 'issue_url': issue_url + }) == expected diff --git a/tests/github_pr_comment/test_legacy_comment.py b/tests/github_pr_comment/test_legacy_comment.py new file mode 100644 index 00000000..045b84ee --- /dev/null +++ b/tests/github_pr_comment/test_legacy_comment.py @@ -0,0 +1,457 @@ +""" +These test verify that _from_api_payload continues to correctly match pre-existing comments, without headers +""" + +import random +import string + +from github_pr_comment.comment import TerraformComment, _from_api_payload + +plan = '''An execution plan has been generated and is shown below. +... +Plan: 1 to add, 0 to change, 0 to destroy.''' + +issue_url = ''.join(random.choice(string.ascii_letters) for _ in range(10)) +comment_url = ''.join(random.choice(string.ascii_letters) for _ in range(10)) + +def test_path_only(): + payload = '''Terraform plan in __/test/terraform__ +
+Plan: 1 to add, 0 to change, 0 to destroy. + +```hcl +An execution plan has been generated and is shown below. +... +Plan: 1 to add, 0 to change, 0 to destroy. +``` +
+ +Testing''' + + expected = TerraformComment( + issue_url=issue_url, + comment_url=comment_url, + status='Testing', + headers={}, + description='Terraform plan in __/test/terraform__', + summary='Plan: 1 to add, 0 to change, 0 to destroy.', + body=plan + ) + + assert _from_api_payload({ + 'body': payload, + 'url': comment_url, + 'issue_url': issue_url + }) == expected + + +def test_nondefault_workspace(): + payload = '''Terraform plan in __/test/terraform__ in the __myworkspace__ workspace +
+Plan: 1 to add, 0 to change, 0 to destroy. + +```hcl +An execution plan has been generated and is shown below. +... +Plan: 1 to add, 0 to change, 0 to destroy. +``` +
+ +Testing''' + + expected = TerraformComment( + issue_url=issue_url, + comment_url=comment_url, + status='Testing', + headers={}, + description='Terraform plan in __/test/terraform__ in the __myworkspace__ workspace', + summary='Plan: 1 to add, 0 to change, 0 to destroy.', + body=plan + ) + + assert _from_api_payload({ + 'body': payload, + 'url': comment_url, + 'issue_url': issue_url + }) == expected + + +def test_variables_single_line(): + payload = '''Terraform plan in __/test/terraform__ +With variables: `var1="value"` +
+Plan: 1 to add, 0 to change, 0 to destroy. + +```hcl +An execution plan has been generated and is shown below. +... +Plan: 1 to add, 0 to change, 0 to destroy. +``` +
+ +Testing''' + + + expected = TerraformComment( + issue_url=issue_url, + comment_url=comment_url, + status='Testing', + headers={}, + description='Terraform plan in __/test/terraform__\nWith variables: `var1="value"`', + summary='Plan: 1 to add, 0 to change, 0 to destroy.', + body=plan + ) + + assert _from_api_payload({ + 'body': payload, + 'url': comment_url, + 'issue_url': issue_url + }) == expected + + +def test_variables_multi_line(): + payload = '''Terraform plan in __/test/terraform__
With variables + +```hcl +var1="value" +var2="value2" +``` +
+ +
+Plan: 1 to add, 0 to change, 0 to destroy. + +```hcl +An execution plan has been generated and is shown below. +... +Plan: 1 to add, 0 to change, 0 to destroy. +``` +
+ +Testing''' + + expected = TerraformComment( + issue_url=issue_url, + comment_url=comment_url, + status='Testing', + headers={}, + description='''Terraform plan in __/test/terraform__
With variables + +```hcl +var1="value" +var2="value2" +``` +
''', + summary='Plan: 1 to add, 0 to change, 0 to destroy.', + body=plan + ) + + assert _from_api_payload({ + 'body': payload, + 'url': comment_url, + 'issue_url': issue_url + }) == expected + + +def test_var(): + payload = '''Terraform plan in __/test/terraform__ +With vars: `var1=value` +
+Plan: 1 to add, 0 to change, 0 to destroy. + +```hcl +An execution plan has been generated and is shown below. +... +Plan: 1 to add, 0 to change, 0 to destroy. +``` +
+ +Testing''' + + expected = TerraformComment( + issue_url=issue_url, + comment_url=comment_url, + status='Testing', + headers={}, + description='''Terraform plan in __/test/terraform__ +With vars: `var1=value`''', + summary='Plan: 1 to add, 0 to change, 0 to destroy.', + body=plan + ) + + assert _from_api_payload({ + 'body': payload, + 'url': comment_url, + 'issue_url': issue_url + }) == expected + + +def test_var_file(): + payload = '''Terraform plan in __/test/terraform__ +With var files: `vars.tf` +
+Plan: 1 to add, 0 to change, 0 to destroy. + +```hcl +An execution plan has been generated and is shown below. +... +Plan: 1 to add, 0 to change, 0 to destroy. +``` +
+ +Testing''' + + expected = TerraformComment( + issue_url=issue_url, + comment_url=comment_url, + status='Testing', + headers={}, + description='''Terraform plan in __/test/terraform__ +With var files: `vars.tf`''', + summary='Plan: 1 to add, 0 to change, 0 to destroy.', + body=plan + ) + + assert _from_api_payload({ + 'body': payload, + 'url': comment_url, + 'issue_url': issue_url + }) == expected + + +def test_backend_config(): + + payload = '''Terraform plan in __/test/terraform__ +With backend config: `bucket=test,key=backend` +
+Plan: 1 to add, 0 to change, 0 to destroy. + +```hcl +An execution plan has been generated and is shown below. +... +Plan: 1 to add, 0 to change, 0 to destroy. +``` +
+ +Testing''' + + expected = TerraformComment( + issue_url=issue_url, + comment_url=comment_url, + status='Testing', + headers={}, + description='''Terraform plan in __/test/terraform__ +With backend config: `bucket=test,key=backend`''', + summary='Plan: 1 to add, 0 to change, 0 to destroy.', + body=plan + ) + + assert _from_api_payload({ + 'body': payload, + 'url': comment_url, + 'issue_url': issue_url + }) == expected + + +def test_backend_config_bad_words(): + payload = '''Terraform plan in __/test/terraform__ +With backend config: `bucket=test,key=backend` +
+Plan: 1 to add, 0 to change, 0 to destroy. + +```hcl +An execution plan has been generated and is shown below. +... +Plan: 1 to add, 0 to change, 0 to destroy. +``` +
+ +Testing''' + + expected = TerraformComment( + issue_url=issue_url, + comment_url=comment_url, + status='Testing', + headers={}, + description='''Terraform plan in __/test/terraform__ +With backend config: `bucket=test,key=backend`''', + summary='Plan: 1 to add, 0 to change, 0 to destroy.', + body=plan + ) + + assert _from_api_payload({ + 'body': payload, + 'url': comment_url, + 'issue_url': issue_url + }) == expected + +def test_target(): + payload = '''Terraform plan in __/test/terraform__ +Targeting resources: `kubernetes_secret.tls_cert_public[0]`, `kubernetes_secret.tls_cert_private` +
+Plan: 1 to add, 0 to change, 0 to destroy. + +```hcl +An execution plan has been generated and is shown below. +... +Plan: 1 to add, 0 to change, 0 to destroy. +``` +
+ +Testing''' + + expected = TerraformComment( + issue_url=issue_url, + comment_url=comment_url, + status='Testing', + headers={}, + description='''Terraform plan in __/test/terraform__ +Targeting resources: `kubernetes_secret.tls_cert_public[0]`, `kubernetes_secret.tls_cert_private`''', + summary='Plan: 1 to add, 0 to change, 0 to destroy.', + body=plan + ) + + assert _from_api_payload({ + 'body': payload, + 'url': comment_url, + 'issue_url': issue_url + }) == expected + +def test_replace(): + payload = '''Terraform plan in __/test/terraform__ +Replacing resources: `kubernetes_secret.tls_cert_public[0]`, `kubernetes_secret.tls_cert_private` +
+Plan: 1 to add, 0 to change, 0 to destroy. + +```hcl +An execution plan has been generated and is shown below. +... +Plan: 1 to add, 0 to change, 0 to destroy. +``` +
+ +Testing''' + + expected = TerraformComment( + issue_url=issue_url, + comment_url=comment_url, + status='Testing', + headers={}, + description='''Terraform plan in __/test/terraform__ +Replacing resources: `kubernetes_secret.tls_cert_public[0]`, `kubernetes_secret.tls_cert_private`''', + summary='Plan: 1 to add, 0 to change, 0 to destroy.', + body=plan + ) + + assert _from_api_payload({ + 'body': payload, + 'url': comment_url, + 'issue_url': issue_url + }) == expected + + +def test_backend_config_file(): + payload = '''Terraform plan in __/test/terraform__ +With backend config files: `backend.tf` +
+Plan: 1 to add, 0 to change, 0 to destroy. + +```hcl +An execution plan has been generated and is shown below. +... +Plan: 1 to add, 0 to change, 0 to destroy. +``` +
+ +Testing''' + + expected = TerraformComment( + issue_url=issue_url, + comment_url=comment_url, + status='Testing', + headers={}, + description='''Terraform plan in __/test/terraform__ +With backend config files: `backend.tf`''', + summary='Plan: 1 to add, 0 to change, 0 to destroy.', + body=plan + ) + + assert _from_api_payload({ + 'body': payload, + 'url': comment_url, + 'issue_url': issue_url + }) == expected + + +def test_all(): + payload = '''Terraform plan in __/test/terraform__ in the __test__ workspace +Targeting resources: `kubernetes_secret.tls_cert_public[0]`, `kubernetes_secret.tls_cert_private` +Replacing resources: `kubernetes_secret.tls_cert_public[0]`, `kubernetes_secret.tls_cert_private` +With backend config: `bucket=mybucket` +With backend config files: `backend.tf` +With vars: `myvar=hello` +With var files: `vars.tf` +
+Plan: 1 to add, 0 to change, 0 to destroy. + +```hcl +An execution plan has been generated and is shown below. +... +Plan: 1 to add, 0 to change, 0 to destroy. +``` +
+ +Testing''' + + expected = TerraformComment( + issue_url=issue_url, + comment_url=comment_url, + status='Testing', + headers={}, + description='''Terraform plan in __/test/terraform__ in the __test__ workspace +Targeting resources: `kubernetes_secret.tls_cert_public[0]`, `kubernetes_secret.tls_cert_private` +Replacing resources: `kubernetes_secret.tls_cert_public[0]`, `kubernetes_secret.tls_cert_private` +With backend config: `bucket=mybucket` +With backend config files: `backend.tf` +With vars: `myvar=hello` +With var files: `vars.tf`''', + summary='Plan: 1 to add, 0 to change, 0 to destroy.', + body=plan + ) + + assert _from_api_payload({ + 'body': payload, + 'url': comment_url, + 'issue_url': issue_url + }) == expected + + +def test_label(): + payload = '''Terraform plan for __test_label__ +
+Plan: 1 to add, 0 to change, 0 to destroy. + +```hcl +An execution plan has been generated and is shown below. +... +Plan: 1 to add, 0 to change, 0 to destroy. +``` +
+ +Testing''' + + expected = TerraformComment( + issue_url=issue_url, + comment_url=comment_url, + status='Testing', + headers={}, + description='''Terraform plan for __test_label__''', + summary='Plan: 1 to add, 0 to change, 0 to destroy.', + body=plan + ) + + assert _from_api_payload({ + 'body': payload, + 'url': comment_url, + 'issue_url': issue_url + }) == expected diff --git a/tests/github_pr_comment/test_summary.py b/tests/github_pr_comment/test_summary.py new file mode 100644 index 00000000..179a6898 --- /dev/null +++ b/tests/github_pr_comment/test_summary.py @@ -0,0 +1,160 @@ +from github_pr_comment.__main__ import create_summary + + +def test_summary_plan_11(): + plan = '''An execution plan has been generated and is shown below. +Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + ++ random_string.my_string + id: + length: "11" + lower: "true" + min_lower: "0" + min_numeric: "0" + min_special: "0" + min_upper: "0" + number: "true" + result: + special: "true" + upper: "true" +Plan: 1 to add, 0 to change, 0 to destroy. +''' + expected = 'Plan: 1 to add, 0 to change, 0 to destroy.' + + assert create_summary(plan) == expected + + +def test_summary_plan_12(): + plan = '''An execution plan has been generated and is shown below. +Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # random_string.my_string will be created + + resource "random_string" "my_string" { + + id = (known after apply) + + length = 11 + + lower = true + + min_lower = 0 + + min_numeric = 0 + + min_special = 0 + + min_upper = 0 + + number = true + + result = (known after apply) + + special = true + + upper = true + } + +Plan: 1 to add, 0 to change, 0 to destroy. +''' + expected = 'Plan: 1 to add, 0 to change, 0 to destroy.' + + assert create_summary(plan) == expected + + +def test_summary_plan_14(): + plan = '''An execution plan has been generated and is shown below. +Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # random_string.my_string will be created + + resource "random_string" "my_string" { + + id = (known after apply) + + length = 11 + + lower = true + + min_lower = 0 + + min_numeric = 0 + + min_special = 0 + + min_upper = 0 + + number = true + + result = (known after apply) + + special = true + + upper = true + } + +Plan: 1 to add, 0 to change, 0 to destroy. + +Changes to Outputs: + + s = "string" +''' + expected = 'Plan: 1 to add, 0 to change, 0 to destroy. Changes to Outputs.' + + assert create_summary(plan) == expected + + +def test_summary_error_11(): + plan = """ +Error: random_string.my_string: length: cannot parse '' as int: strconv.ParseInt: parsing "ten": invalid syntax + +""" + expected = "Error: random_string.my_string: length: cannot parse '' as int: strconv.ParseInt: parsing \"ten\": invalid syntax" + + assert create_summary(plan) == expected + + +def test_summary_error_12(): + plan = """ +Error: Incorrect attribute value type + + on main.tf line 2, in resource "random_string" "my_string": + 2: length = "ten" + +Inappropriate value for attribute "length": a number is required. +""" + + expected = "Error: Incorrect attribute value type" + assert create_summary(plan) == expected + + +def test_summary_no_change_11(): + plan = """No changes. Infrastructure is up-to-date. + +This means that Terraform did not detect any differences between your +configuration and real physical resources that exist. As a result, no +actions need to be performed. +""" + + expected = "No changes. Infrastructure is up-to-date." + assert create_summary(plan) == expected + + +def test_summary_no_change_14(): + plan = """No changes. Infrastructure is up-to-date. + +This means that Terraform did not detect any differences between your +configuration and real physical resources that exist. As a result, no +actions need to be performed. +""" + + expected = "No changes. Infrastructure is up-to-date." + assert create_summary(plan) == expected + + +def test_summary_output_only_change_14(): + plan = """An execution plan has been generated and is shown below. +Resource actions are indicated with the following symbols: + +Terraform will perform the following actions: + +Plan: 0 to add, 0 to change, 0 to destroy. + +Changes to Outputs: + + hello = "world" + +""" + + expected = "Plan: 0 to add, 0 to change, 0 to destroy. Changes to Outputs." + assert create_summary(plan) == expected + + +def test_summary_unknown(): + plan = """ +This is not anything like terraform output we know. We don't want to generate a summary for this. +""" + assert create_summary(plan) is None diff --git a/tests/test_pr_comment.py b/tests/test_pr_comment.py deleted file mode 100644 index f49ce1bd..00000000 --- a/tests/test_pr_comment.py +++ /dev/null @@ -1,535 +0,0 @@ -from github_pr_comment import format_body, ActionInputs, create_summary - -plan = '''An execution plan has been generated and is shown below. -... -Plan: 1 to add, 0 to change, 0 to destroy.''' - - -def action_inputs(*, - path='/test/terraform', - workspace='default', - backend_config='', - backend_config_file='', - variables='', - var='', - var_file='', - label='', - target='', - replace='' - ) -> ActionInputs: - return ActionInputs( - INPUT_WORKSPACE=workspace, - INPUT_PATH=path, - INPUT_BACKEND_CONFIG=backend_config, - INPUT_BACKEND_CONFIG_FILE=backend_config_file, - INPUT_VARIABLES=variables, - INPUT_VAR=var, - INPUT_VAR_FILE=var_file, - INPUT_LABEL=label, - INPUT_ADD_GITHUB_COMMENT='true', - INPUT_TARGET=target, - INPUT_REPLACE=replace - ) - - -def test_path_only(): - inputs = action_inputs( - path='/test/terraform' - ) - - status = 'Testing' - - expected = '''Terraform plan in __/test/terraform__ -
-Plan: 1 to add, 0 to change, 0 to destroy. - -```hcl -An execution plan has been generated and is shown below. -... -Plan: 1 to add, 0 to change, 0 to destroy. -``` -
- -Testing''' - - assert format_body(inputs, plan, status, 10).splitlines() == expected.splitlines() - - -def test_nondefault_workspace(): - inputs = action_inputs( - path='/test/terraform', - workspace='myworkspace' - ) - - status = 'Testing' - - expected = '''Terraform plan in __/test/terraform__ in the __myworkspace__ workspace -
-Plan: 1 to add, 0 to change, 0 to destroy. - -```hcl -An execution plan has been generated and is shown below. -... -Plan: 1 to add, 0 to change, 0 to destroy. -``` -
- -Testing''' - - assert format_body(inputs, plan, status, 10).splitlines() == expected.splitlines() - - -def test_variables_single_line(): - inputs = action_inputs( - path='/test/terraform', - variables='var1="value"' - ) - - status = 'Testing' - - expected = '''Terraform plan in __/test/terraform__ -With variables: `var1="value"` -
-Plan: 1 to add, 0 to change, 0 to destroy. - -```hcl -An execution plan has been generated and is shown below. -... -Plan: 1 to add, 0 to change, 0 to destroy. -``` -
- -Testing''' - - assert format_body(inputs, plan, status, 10).splitlines() == expected.splitlines() - - -def test_variables_multi_line(): - inputs = action_inputs( - path='/test/terraform', - variables='''var1="value" -var2="value2"''' - ) - - status = 'Testing' - - expected = '''Terraform plan in __/test/terraform__
With variables - -```hcl -var1="value" -var2="value2" -``` -
- -
-Plan: 1 to add, 0 to change, 0 to destroy. - -```hcl -An execution plan has been generated and is shown below. -... -Plan: 1 to add, 0 to change, 0 to destroy. -``` -
- -Testing''' - - assert format_body(inputs, plan, status, 10).splitlines() == expected.splitlines() - - -def test_var(): - inputs = action_inputs( - path='/test/terraform', - var='var1=value' - ) - - status = 'Testing' - - expected = '''Terraform plan in __/test/terraform__ -With vars: `var1=value` -
-Plan: 1 to add, 0 to change, 0 to destroy. - -```hcl -An execution plan has been generated and is shown below. -... -Plan: 1 to add, 0 to change, 0 to destroy. -``` -
- -Testing''' - - assert format_body(inputs, plan, status, 10).splitlines() == expected.splitlines() - - -def test_var_file(): - inputs = action_inputs( - path='/test/terraform', - var_file='vars.tf' - ) - - status = 'Testing' - - expected = '''Terraform plan in __/test/terraform__ -With var files: `vars.tf` -
-Plan: 1 to add, 0 to change, 0 to destroy. - -```hcl -An execution plan has been generated and is shown below. -... -Plan: 1 to add, 0 to change, 0 to destroy. -``` -
- -Testing''' - - assert format_body(inputs, plan, status, 10).splitlines() == expected.splitlines() - - -def test_backend_config(): - inputs = action_inputs( - path='/test/terraform', - backend_config='bucket=test,key=backend' - ) - status = 'Testing' - - expected = '''Terraform plan in __/test/terraform__ -With backend config: `bucket=test,key=backend` -
-Plan: 1 to add, 0 to change, 0 to destroy. - -```hcl -An execution plan has been generated and is shown below. -... -Plan: 1 to add, 0 to change, 0 to destroy. -``` -
- -Testing''' - - assert format_body(inputs, plan, status, 10).splitlines() == expected.splitlines() - - -def test_backend_config_bad_words(): - inputs = action_inputs( - path='/test/terraform', - backend_config='bucket=test,password=secret,key=backend,token=secret' - ) - - status = 'Testing' - - expected = '''Terraform plan in __/test/terraform__ -With backend config: `bucket=test,key=backend` -
-Plan: 1 to add, 0 to change, 0 to destroy. - -```hcl -An execution plan has been generated and is shown below. -... -Plan: 1 to add, 0 to change, 0 to destroy. -``` -
- -Testing''' - - assert format_body(inputs, plan, status, 10).splitlines() == expected.splitlines() - -def test_target(): - inputs = action_inputs( - path='/test/terraform', - target='''kubernetes_secret.tls_cert_public[0] -kubernetes_secret.tls_cert_private''' - ) - - status = 'Testing' - - expected = '''Terraform plan in __/test/terraform__ -Targeting resources: `kubernetes_secret.tls_cert_public[0]`, `kubernetes_secret.tls_cert_private` -
-Plan: 1 to add, 0 to change, 0 to destroy. - -```hcl -An execution plan has been generated and is shown below. -... -Plan: 1 to add, 0 to change, 0 to destroy. -``` -
- -Testing''' - - assert format_body(inputs, plan, status, 10).splitlines() == expected.splitlines() - -def test_replace(): - inputs = action_inputs( - path='/test/terraform', - replace='''kubernetes_secret.tls_cert_public[0] -kubernetes_secret.tls_cert_private''' - ) - - status = 'Testing' - - expected = '''Terraform plan in __/test/terraform__ -Replacing resources: `kubernetes_secret.tls_cert_public[0]`, `kubernetes_secret.tls_cert_private` -
-Plan: 1 to add, 0 to change, 0 to destroy. - -```hcl -An execution plan has been generated and is shown below. -... -Plan: 1 to add, 0 to change, 0 to destroy. -``` -
- -Testing''' - - assert format_body(inputs, plan, status, 10).splitlines() == expected.splitlines() - -def test_backend_config_file(): - inputs = action_inputs( - path='/test/terraform', - backend_config_file='backend.tf' - ) - - status = 'Testing' - - expected = '''Terraform plan in __/test/terraform__ -With backend config files: `backend.tf` -
-Plan: 1 to add, 0 to change, 0 to destroy. - -```hcl -An execution plan has been generated and is shown below. -... -Plan: 1 to add, 0 to change, 0 to destroy. -``` -
- -Testing''' - - assert format_body(inputs, plan, status, 10).splitlines() == expected.splitlines() - - -def test_all(): - inputs = action_inputs( - path='/test/terraform', - workspace='test', - var='myvar=hello', - var_file='vars.tf', - backend_config='bucket=mybucket,password=secret', - backend_config_file='backend.tf', - target = '''kubernetes_secret.tls_cert_public[0] -kubernetes_secret.tls_cert_private''', - replace='''kubernetes_secret.tls_cert_public[0] -kubernetes_secret.tls_cert_private''' - ) - - status = 'Testing' - - expected = '''Terraform plan in __/test/terraform__ in the __test__ workspace -Targeting resources: `kubernetes_secret.tls_cert_public[0]`, `kubernetes_secret.tls_cert_private` -Replacing resources: `kubernetes_secret.tls_cert_public[0]`, `kubernetes_secret.tls_cert_private` -With backend config: `bucket=mybucket` -With backend config files: `backend.tf` -With vars: `myvar=hello` -With var files: `vars.tf` -
-Plan: 1 to add, 0 to change, 0 to destroy. - -```hcl -An execution plan has been generated and is shown below. -... -Plan: 1 to add, 0 to change, 0 to destroy. -``` -
- -Testing''' - - assert format_body(inputs, plan, status, 10).splitlines() == expected.splitlines() - - -def test_label(): - inputs = action_inputs( - path='/test/terraform', - workspace='test', - var='myvar=hello', - var_file='vars.tf', - backend_config='bucket=mybucket,password=secret', - backend_config_file='backend.tf', - label='test_label' - ) - - status = 'Testing' - - expected = '''Terraform plan for __test_label__ -
-Plan: 1 to add, 0 to change, 0 to destroy. - -```hcl -An execution plan has been generated and is shown below. -... -Plan: 1 to add, 0 to change, 0 to destroy. -``` -
- -Testing''' - - assert format_body(inputs, plan, status, 10).splitlines() == expected.splitlines() - - -def test_summary_plan_11(): - plan = '''An execution plan has been generated and is shown below. -Resource actions are indicated with the following symbols: - + create - -Terraform will perform the following actions: - -+ random_string.my_string - id: - length: "11" - lower: "true" - min_lower: "0" - min_numeric: "0" - min_special: "0" - min_upper: "0" - number: "true" - result: - special: "true" - upper: "true" -Plan: 1 to add, 0 to change, 0 to destroy. -''' - expected = 'Plan: 1 to add, 0 to change, 0 to destroy.' - - assert create_summary(plan) == expected - - -def test_summary_plan_12(): - plan = '''An execution plan has been generated and is shown below. -Resource actions are indicated with the following symbols: - + create - -Terraform will perform the following actions: - - # random_string.my_string will be created - + resource "random_string" "my_string" { - + id = (known after apply) - + length = 11 - + lower = true - + min_lower = 0 - + min_numeric = 0 - + min_special = 0 - + min_upper = 0 - + number = true - + result = (known after apply) - + special = true - + upper = true - } - -Plan: 1 to add, 0 to change, 0 to destroy. -''' - expected = 'Plan: 1 to add, 0 to change, 0 to destroy.' - - assert create_summary(plan) == expected - - -def test_summary_plan_14(): - plan = '''An execution plan has been generated and is shown below. -Resource actions are indicated with the following symbols: - + create - -Terraform will perform the following actions: - - # random_string.my_string will be created - + resource "random_string" "my_string" { - + id = (known after apply) - + length = 11 - + lower = true - + min_lower = 0 - + min_numeric = 0 - + min_special = 0 - + min_upper = 0 - + number = true - + result = (known after apply) - + special = true - + upper = true - } - -Plan: 1 to add, 0 to change, 0 to destroy. - -Changes to Outputs: - + s = "string" -''' - expected = 'Plan: 1 to add, 0 to change, 0 to destroy. Changes to Outputs.' - - assert create_summary(plan) == expected - - -def test_summary_error_11(): - plan = """ -Error: random_string.my_string: length: cannot parse '' as int: strconv.ParseInt: parsing "ten": invalid syntax - -""" - expected = "Error: random_string.my_string: length: cannot parse '' as int: strconv.ParseInt: parsing \"ten\": invalid syntax" - - assert create_summary(plan) == expected - - -def test_summary_error_12(): - plan = """ -Error: Incorrect attribute value type - - on main.tf line 2, in resource "random_string" "my_string": - 2: length = "ten" - -Inappropriate value for attribute "length": a number is required. -""" - - expected = "Error: Incorrect attribute value type" - assert create_summary(plan) == expected - - -def test_summary_no_change_11(): - plan = """No changes. Infrastructure is up-to-date. - -This means that Terraform did not detect any differences between your -configuration and real physical resources that exist. As a result, no -actions need to be performed. -""" - - expected = "No changes. Infrastructure is up-to-date." - assert create_summary(plan) == expected - - -def test_summary_no_change_14(): - plan = """No changes. Infrastructure is up-to-date. - -This means that Terraform did not detect any differences between your -configuration and real physical resources that exist. As a result, no -actions need to be performed. -""" - - expected = "No changes. Infrastructure is up-to-date." - assert create_summary(plan) == expected - - -def test_summary_output_only_change_14(): - plan = """An execution plan has been generated and is shown below. -Resource actions are indicated with the following symbols: - -Terraform will perform the following actions: - -Plan: 0 to add, 0 to change, 0 to destroy. - -Changes to Outputs: - + hello = "world" - -""" - - expected = "Plan: 0 to add, 0 to change, 0 to destroy. Changes to Outputs." - assert create_summary(plan) == expected - - -def test_summary_unknown(): - plan = """ -This is not anything like terraform output we know. We don't want to generate a summary for this. -""" - assert create_summary(plan) is None From 47a58e58053db41e1381c78febe6c743c02a4aea Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Fri, 4 Mar 2022 23:29:10 +0000 Subject: [PATCH 02/18] Restore comment step cache --- image/src/github_pr_comment/__main__.py | 13 +++++++++---- image/src/github_pr_comment/comment.py | 24 +++++++++++++++++++++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/image/src/github_pr_comment/__main__.py b/image/src/github_pr_comment/__main__.py index 74719391..a874819a 100644 --- a/image/src/github_pr_comment/__main__.py +++ b/image/src/github_pr_comment/__main__.py @@ -10,7 +10,7 @@ from github_actions.env import GithubEnv from github_actions.find_pr import find_pr, WorkflowException from github_actions.inputs import PlanPrInputs -from github_pr_comment.comment import find_comment, TerraformComment, update_comment +from github_pr_comment.comment import find_comment, TerraformComment, update_comment, serialize, deserialize Plan = NewType('Plan', str) Status = NewType('Status', str) @@ -171,6 +171,9 @@ def get_pr() -> PrUrl: def get_comment(action_inputs: PlanPrInputs) -> TerraformComment: + if 'comment' in step_cache: + return deserialize(step_cache['comment']) + pr_url = get_pr() issue_url = get_issue_url(pr_url) username = current_user(env) @@ -190,7 +193,6 @@ def get_comment(action_inputs: PlanPrInputs) -> TerraformComment: return find_comment(github, issue_url, username, headers, legacy_description) - def main() -> int: if len(sys.argv) < 2: sys.stderr.write(f'''Usage: @@ -220,7 +222,7 @@ def main() -> int: debug('Comment doesn\'t already exist - not creating it') return 0 - update_comment( + comment = update_comment( github, comment, description=description, @@ -231,17 +233,20 @@ def main() -> int: elif sys.argv[1] == 'status': if comment.comment_url is None: + debug("Can't set status of comment that doesn't exist") return 1 else: - update_comment(github, comment, status=status) + comment = update_comment(github, comment, status=status) elif sys.argv[1] == 'get': if comment.comment_url is None: + debug("Can't get the plan from comment that doesn't exist") return 1 with open(sys.argv[2], 'w') as f: f.write(comment.body) + step_cache['comment'] = serialize(comment) if __name__ == '__main__': sys.exit(main()) diff --git a/image/src/github_pr_comment/comment.py b/image/src/github_pr_comment/comment.py index 57a130c3..d8d7bd7f 100644 --- a/image/src/github_pr_comment/comment.py +++ b/image/src/github_pr_comment/comment.py @@ -12,7 +12,6 @@ except (ValueError, KeyError): collapse_threshold = 10 - class TerraformComment: """ Represents a Terraform PR comment @@ -87,6 +86,29 @@ def body(self) -> str: def status(self) -> str: return self._status +def serialize(comment: TerraformComment) -> str: + return json.dumps({ + 'issue_url': comment.issue_url, + 'comment_url': comment.comment_url, + 'headers': comment.headers, + 'description': comment.description, + 'summary': comment.summary, + 'body': comment.body, + 'status': comment.status + }) + +def deserialize(s) -> TerraformComment: + j = json.loads(s) + + return TerraformComment( + issue_url=j['issue_url'], + comment_url=j['comment_url'], + headers=j['headers'], + description=j['description'], + summary=j['summary'], + body=j['body'], + status=j['status'] + ) def _format_comment_header(**kwargs) -> str: return f'' From 4d4b5f47360b3c283b6aba663047a923aa5e8897 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sat, 5 Mar 2022 08:26:37 +0000 Subject: [PATCH 03/18] Restore comment step cache --- .github/github_sucks.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/github_sucks.md b/.github/github_sucks.md index 471620e2..cee65b7f 100644 --- a/.github/github_sucks.md +++ b/.github/github_sucks.md @@ -1,2 +1,3 @@ Everytime I need to generate a push or synchronise event I will touch this file. This is usually because GitHub Actions has broken in some way. + From 28b6681ab0175da77179ec8318dfd63882c9cafe Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sat, 5 Mar 2022 09:18:33 +0000 Subject: [PATCH 04/18] Don't use comments with headers as backup candidates --- image/src/github_pr_comment/comment.py | 38 ++++++++++++++++++++------ 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/image/src/github_pr_comment/comment.py b/image/src/github_pr_comment/comment.py index d8d7bd7f..4361e81e 100644 --- a/image/src/github_pr_comment/comment.py +++ b/image/src/github_pr_comment/comment.py @@ -189,6 +189,21 @@ def _to_api_payload(comment: TerraformComment) -> str: return body +def matching_headers(comment: TerraformComment, headers: dict[str, str]) -> bool: + """ + Does a comment have all the specified headers + + Additional headers may be present in the comment, they are ignored if not specified in the headers argument. + """ + + for header, value in headers.items(): + if header not in comment.headers: + return False + + if comment.headers[header] != value: + return False + + return True def find_comment(github: GithubApi, issue_url: IssueUrl, username: str, headers: dict[str, str], legacy_description: str) -> TerraformComment: """ @@ -212,20 +227,27 @@ def find_comment(github: GithubApi, issue_url: IssueUrl, username: str, headers: if comment_payload['user']['login'] != username: continue - debug(json.dumps(comment_payload)) + #debug(json.dumps(comment_payload)) if comment := _from_api_payload(comment_payload): - if comment.headers == headers: - debug('Found comment that matches headers') - return comment + if comment.headers: + # Match by headers only + + if matching_headers(comment, headers): + debug('Found comment that matches headers') + return comment + + debug(f"Didn't match comment with {comment.headers=}") - debug(f"Didn't match comment with {comment.headers=}") + else: + # Match by description only - if comment.description == legacy_description: - backup_comment = comment + if comment.description == legacy_description and backup_comment is None: + debug('Found backup comment that matches legacy description') + backup_comment = comment - debug(f"Didn't match comment with {comment.description=}") + debug(f"Didn't match comment with {comment.description=}") if backup_comment is not None: debug('Found comment matching legacy description') From a86fa1d77b4dbe1ab933a6dafa9974cba3e14d04 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Fri, 18 Mar 2022 19:43:04 +0000 Subject: [PATCH 05/18] Add backend fingerprint comment header --- image/setup.py | 3 +- image/src/github_pr_comment/__main__.py | 22 +- image/src/github_pr_comment/backend_config.py | 51 +++++ .../github_pr_comment/backend_fingerprint.py | 195 ++++++++++++++++++ image/src/github_pr_comment/comment.py | 8 +- 5 files changed, 270 insertions(+), 9 deletions(-) create mode 100644 image/src/github_pr_comment/backend_config.py create mode 100644 image/src/github_pr_comment/backend_fingerprint.py diff --git a/image/setup.py b/image/setup.py index d810109c..92ed5122 100644 --- a/image/setup.py +++ b/image/setup.py @@ -16,6 +16,7 @@ }, install_requires=[ 'requests', - 'python-hcl2' + 'python-hcl2', + 'canonicaljson' ] ) diff --git a/image/src/github_pr_comment/__main__.py b/image/src/github_pr_comment/__main__.py index a874819a..41237114 100644 --- a/image/src/github_pr_comment/__main__.py +++ b/image/src/github_pr_comment/__main__.py @@ -2,6 +2,7 @@ import json import os import sys +from pathlib import Path from typing import (NewType, Optional, cast) from github_actions.api import GithubApi, IssueUrl, PrUrl @@ -10,7 +11,10 @@ from github_actions.env import GithubEnv from github_actions.find_pr import find_pr, WorkflowException from github_actions.inputs import PlanPrInputs +from github_pr_comment.backend_config import complete_config +from github_pr_comment.backend_fingerprint import fingerprint from github_pr_comment.comment import find_comment, TerraformComment, update_comment, serialize, deserialize +from terraform.module import load_module Plan = NewType('Plan', str) Status = NewType('Status', str) @@ -169,8 +173,12 @@ def get_pr() -> PrUrl: return cast(PrUrl, pr_url) +def comment_hash(value: str, salt: str) -> str: + h = hashlib.sha256(f'dflook/terraform-github-actions/{salt}') + h.update(value) + return h.hexdigest() -def get_comment(action_inputs: PlanPrInputs) -> TerraformComment: +def get_comment(action_inputs: PlanPrInputs, backend_fingerprint: str) -> TerraformComment: if 'comment' in step_cache: return deserialize(step_cache['comment']) @@ -182,14 +190,14 @@ def get_comment(action_inputs: PlanPrInputs) -> TerraformComment: headers = { 'workspace': os.environ.get('INPUT_WORKSPACE', 'default'), - 'backend': hashlib.sha256(legacy_description.encode()).hexdigest() + 'backend': comment_hash(backend_fingerprint, pr_url) } if backend_type := os.environ.get('TERRAFORM_BACKEND_TYPE'): headers['backend_type'] = backend_type if label := os.environ.get('INPUT_LABEL'): - headers['label'] = hashlib.sha256(label.encode()).hexdigest() + headers['label'] = label return find_comment(github, issue_url, username, headers, legacy_description) @@ -206,7 +214,13 @@ def main() -> int: action_inputs = cast(PlanPrInputs, os.environ) - comment = get_comment(action_inputs) + module = load_module(Path(action_inputs.get('INPUT_PATH', '.'))) + + backend_type, backend_config = complete_config(action_inputs, module) + + backend_fingerprint = fingerprint(backend_type, backend_config, os.environ) + + comment = get_comment(action_inputs, backend_fingerprint) status = cast(Status, os.environ.get('STATUS', '')) diff --git a/image/src/github_pr_comment/backend_config.py b/image/src/github_pr_comment/backend_config.py new file mode 100644 index 00000000..a12a3bdf --- /dev/null +++ b/image/src/github_pr_comment/backend_config.py @@ -0,0 +1,51 @@ +import re +from typing import Tuple, Any + +from github_actions.debug import debug +from github_actions.inputs import InitInputs +from terraform.module import TerraformModule + +BackendConfig = dict[str, Any] +BackendType = str + + +def partial_backend_config(module: TerraformModule) -> Tuple[BackendType, BackendConfig]: + """Return the backend config specified in the terraform module.""" + + for terraform in module.get('terraform', []): + for backend in terraform.get('backend', []): + for backend_type, config in backend.items(): + return backend_type, config + + for cloud in terraform.get('cloud', []): + return 'cloud', cloud + + return 'local', {} + + +def read_backend_config_vars(init_inputs: InitInputs) -> BackendConfig: + """Read any backend config from input variables.""" + + config: BackendConfig = {} + + for path in init_inputs.get('INPUT_BACKEND_CONFIG_FILE', '').replace(',', '\n').splitlines(): + try: + config |= load_backend_config_file(Path(path)) # type: ignore + except Exception as e: + debug(f'Failed to load backend config file {path}') + debug(str(e)) + + for backend_var in init_inputs.get('INPUT_BACKEND_CONFIG', '').replace(',', '\n').splitlines(): + if match := re.match(r'(.*)\s*=\s*(.*)', backend_var): + config[match.group(1)] = match.group(2) + + return config + + +def complete_config(action_inputs: InitInputs, module: TerraformModule) -> Tuple[BackendType, BackendConfig]: + backend_type, config = partial_backend_config(module) + + for key, value in read_backend_config_vars(action_inputs): + config[key] = value + + return backend_type, config diff --git a/image/src/github_pr_comment/backend_fingerprint.py b/image/src/github_pr_comment/backend_fingerprint.py new file mode 100644 index 00000000..c775901c --- /dev/null +++ b/image/src/github_pr_comment/backend_fingerprint.py @@ -0,0 +1,195 @@ +""" +Backend fingerprinting + +Given a completed backend config and environment variables, compute a fingerprint that identifies that backend config. +This disregards any config related to *how* that backend is used. + +Combined with the backend type and workspace name, this should uniquely identify a remote state file. + +""" +import canonicaljson + +from github_pr_comment.backend_config import BackendConfig, BackendType + + +def fingerprint_remote(backend_config: BackendConfig, env) -> dict[str, str]: + return { + 'hostname': backend_config.get('hostname', ''), + 'organization': backend_config.get('organization', ''), + 'workspaces': backend_config.get('workspaces', '') + } + + +def fingerprint_cloud(backend_config: BackendConfig, env) -> dict[str, str]: + return { + 'hostname': backend_config.get('hostname', ''), + 'organization': backend_config.get('organization', ''), + 'workspaces': backend_config.get('workspaces', '') + } + + +def fingerprint_artifactory(backend_config: BackendConfig, env) -> dict[str, str]: + return { + 'url': backend_config.get('url') or env.get('ARTIFACTORY_URL', ''), + 'repo': backend_config.get('repo', ''), + 'subpath': backend_config.get('subpath', '') + } + + +def fingerprint_azurerm(backend_config: BackendConfig, env) -> dict[str, str]: + return { + 'storage_account_name': backend_config.get('storage_account_name', ''), + 'container_name': backend_config.get('container_name', ''), + 'key': backend_config.get('key', ''), + 'environment': backend_config.get('environment') or env.get('ARM_ENVIRONMENT', ''), + 'endpoint': backend_config.get('endpoint') or env.get('ARM_ENDPOINT', ''), + 'resource_group_name': backend_config.get('resource_group_name', ''), + 'msi_endpoint': backend_config.get('msi_endpoint') or env.get('ARM_MSI_ENDPOINT', ''), + 'subscription_id': backend_config.get('subscription_id') or env.get('ARM_SUBSCRIPTION_ID', ''), + 'tenant_id': backend_config.get('tenant_id') or env.get('ARM_TENANT_ID', ''), + } + + +def fingerprint_consul(backend_config: BackendConfig, env) -> dict[str, str]: + return { + 'path': backend_config.get('path', ''), + 'address': backend_config.get('address') or env.get('CONSUL_HTTP_ADDR', ''), + } + + +def fingerprint_cos(backend_config: BackendConfig, env) -> dict[str, str]: + return { + 'bucket': backend_config.get('bucket', ''), + 'prefix': backend_config.get('prefix', ''), + 'key': backend_config.get('key', ''), + 'region': backend_config.get('region', '') + } + + +def fingerprint_etcd(backend_config: BackendConfig, env) -> dict[str, str]: + return { + 'path': backend_config.get('path', ''), + 'endpoints': ' '.join(sorted(backend_config.get('endpoints', '').split(' '))) + } + + +def fingerprint_etcd3(backend_config: BackendConfig, env) -> dict[str, str]: + return { + 'prefix': backend_config.get('prefix', ''), + 'endpoints': ' '.join(sorted(backend_config.get('endpoints', []))) + } + + +def fingerprint_gcs(backend_config: BackendConfig, env) -> dict[str, str]: + return { + 'bucket': backend_config.get('bucket', ''), + 'prefix': backend_config.get('prefix', '') + } + + +def fingerprint_http(backend_config: BackendConfig, env) -> dict[str, str]: + return { + 'address': backend_config.get('address') or env.get('TF_HTTP_ADDRESS', ''), + 'lock_address': backend_config.get('lock_address') or env.get('TF_HTTP_LOCK_ADDRESS', ''), + 'unlock_address': backend_config.get('unlock_address') or env.get('TF_HTTP_UNLOCK_ADDRESS', ''), + } + + +def fingerprint_kubernetes(backend_config: BackendConfig, env) -> dict[str, str]: + return { + 'secret_suffix': backend_config.get('secret_suffix', ''), + 'namespace': backend_config.get('namespace') or env.get('KUBE_NAMESPACE', ''), + 'host': backend_config.get('host') or env.get('KUBE_HOST', ''), + 'config_path': backend_config.get('config_path') or env.get('KUBE_CONFIG_PATH', ''), + 'config_paths': backend_config.get('config_paths') or env.get('KUBE_CONFIG_PATHS', ''), + 'config_context': backend_config.get('context') or env.get('KUBE_CTX', ''), + 'config_context_cluster': backend_config.get('config_context_cluster') or env.get('KUBE_CTX_CLUSTER', '') + } + + +def fingerprint_manta(backend_config: BackendConfig, env) -> dict[str, str]: + return { + 'account': backend_config.get('account') or env.get('SDC_ACCOUNT') or env.get('TRITON_ACCOUNT', ''), + 'url': backend_config.get('url') or env.get('MANTA_URL', ''), + 'path': backend_config.get('path', ''), + 'object_name': backend_config.get('object_name', '') + } + + +def fingerprint_oss(backend_config: BackendConfig, env) -> dict[str, str]: + return { + 'region': backend_config.get('region') or env.get('ALICLOUD_REGION') or env.get('ALICLOUD_DEFAULT_REGION', ''), + 'endpoint': backend_config.get('endpoint') or env.get('ALICLOUD_OSS_ENDPOINT') or env.get('OSS_ENDPOINT', ''), + 'bucket': backend_config.get('bucket', ''), + 'prefix': backend_config.get('prefix', ''), + 'key': backend_config.get('key', ''), + } + + +def fingerprint_pg(backend_config: BackendConfig, env) -> dict[str, str]: + return { + 'conn_str': backend_config.get('conn_str', ''), + 'schema_name': backend_config.get('schema_name', '') + } + + +def fingerprint_s3(backend_config: BackendConfig, env) -> dict[str, str]: + return { + 'endpoint': backend_config.get('endpoint') or env.get('AWS_S3_ENDPOINT', ''), + 'bucket': backend_config.get('bucket', ''), + 'workspace_key_prefix': backend_config.get('workspace_key_prefix', ''), + 'key': backend_config.get('key', ''), + } + + +def fingerprint_swift(backend_config: BackendConfig, env) -> dict[str, str]: + return { + 'auth_url': backend_config.get('auth_url') or env.get('OS_AUTH_URL', ''), + 'cloud': backend_config.get('cloud') or env.get('OS_CLOUD', ''), + 'region_name': backend_config.get('region_name') or env.get('OS_REGION_NAME', ''), + 'container': backend_config.get('container', ''), + 'state_name': backend_config.get('state_name', ''), + 'path': backend_config.get('path', ''), + 'tenant_id': backend_config.get('tenant_id') or env.get('OS_TENANT_NAME') or env.get('OS_PROJECT_NAME', ''), + 'project_domain_name': backend_config.get('project_domain_name') or env.get('OS_PROJECT_DOMAIN_NAME', ''), + 'project_domain_id': backend_config.get('project_domain_id') or env.get('OS_PROJECT_DOMAIN_ID', ''), + 'domain_name': backend_config.get('domain_name') or env.get('OS_USER_DOMAIN_NAME') or env.get('OS_PROJECT_DOMAIN_NAME') or env.get('OS_DOMAIN_NAME') or env.get('DEFAULT_DOMAIN'), + 'domain_id': backend_config.get('domain_id') or env.get('OS_PROJECT_DOMAIN_ID', ''), + 'default_domain': backend_config.get('default_domain') or env.get('OS_DEFAULT_DOMAIN', '') + } + + +def fingerprint_local(backend_config: BackendConfig, env) -> dict[str, str]: + fingerprint_inputs = { + 'path': backend_config.get('path', env['INPUT_PATH']) + } + + if 'workspace_dir' in backend_config: + fingerprint_inputs['workspace_dir'] = backend_config['workspace_dir'] + + return fingerprint_inputs + + +def fingerprint(backend_type: BackendType, backend_config: BackendConfig, env) -> str: + backends = { + 'remote': fingerprint_remote, + 'artifactory': fingerprint_artifactory, + 'azurerm': fingerprint_azurerm, + 'consul': fingerprint_consul, + 'cloud': fingerprint_cloud, + 'cos': fingerprint_cos, + 'etcd': fingerprint_etcd, + 'etcd3': fingerprint_etcd3, + 'gcs': fingerprint_gcs, + 'http': fingerprint_http, + 'kubernetes': fingerprint_kubernetes, + 'manta': fingerprint_manta, + 'oss': fingerprint_oss, + 'pg': fingerprint_pg, + 's3': fingerprint_s3, + 'swift': fingerprint_swift, + 'local': fingerprint_local, + } + + fingerprint_inputs = backends.get(backend_type, lambda c: c)(backend_config, env) + return canonicaljson.encode_canonical_json(fingerprint_inputs) diff --git a/image/src/github_pr_comment/comment.py b/image/src/github_pr_comment/comment.py index 4361e81e..8c2cfacb 100644 --- a/image/src/github_pr_comment/comment.py +++ b/image/src/github_pr_comment/comment.py @@ -127,7 +127,7 @@ def _parse_comment_header(comment_header: Optional[str]) -> dict[str, str]: def _from_api_payload(comment: dict[str, Any]) -> Optional[TerraformComment]: - match = re.match(rf''' + match = re.match(r''' (?P\n)? (?P.*) \s* @@ -138,9 +138,9 @@ def _from_api_payload(comment: dict[str, Any]) -> Optional[TerraformComment]: (?P.*) ''', - comment['body'], - re.VERBOSE | re.DOTALL - ) + comment['body'], + re.VERBOSE | re.DOTALL + ) if not match: return None From 75c1c2daa2644b43a2f08c4be46412a982a5481f Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Fri, 18 Mar 2022 19:45:24 +0000 Subject: [PATCH 06/18] Add canonicaljson --- tests/requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/requirements.txt b/tests/requirements.txt index 1e3dab6f..1282130b 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,8 +1,7 @@ requests pytest python-hcl2 - +canonicaljson types-requests - mypy flake8 From fbcdb839ddbdaa0d03e1487361890044f51602cd Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Fri, 18 Mar 2022 20:02:30 +0000 Subject: [PATCH 07/18] Typecheck --- image/src/github_actions/find_pr.py | 3 +-- image/src/github_pr_comment/__main__.py | 11 ++++++----- image/src/github_pr_comment/backend_config.py | 2 +- image/src/github_pr_comment/backend_fingerprint.py | 2 +- image/src/github_pr_comment/comment.py | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/image/src/github_actions/find_pr.py b/image/src/github_actions/find_pr.py index 0fefb4bc..9970fcfd 100644 --- a/image/src/github_actions/find_pr.py +++ b/image/src/github_actions/find_pr.py @@ -72,5 +72,4 @@ def prs() -> Iterable[dict[str, Any]]: raise WorkflowException(f'No PR found in {repo} for commit {commit} (was it pushed directly to the target branch?)') - else: - raise WorkflowException(f"The {event_type} event doesn\'t relate to a Pull Request.") + raise WorkflowException(f"The {event_type} event doesn\'t relate to a Pull Request.") diff --git a/image/src/github_pr_comment/__main__.py b/image/src/github_pr_comment/__main__.py index 41237114..23a1904f 100644 --- a/image/src/github_pr_comment/__main__.py +++ b/image/src/github_pr_comment/__main__.py @@ -19,12 +19,12 @@ Plan = NewType('Plan', str) Status = NewType('Status', str) -job_cache = ActionsCache(os.environ.get('JOB_TMP_DIR', '.'), 'job_cache') -step_cache = ActionsCache(os.environ.get('STEP_TMP_DIR', '.'), 'step_cache') +job_cache = ActionsCache(Path(os.environ.get('JOB_TMP_DIR', '.')), 'job_cache') +step_cache = ActionsCache(Path(os.environ.get('STEP_TMP_DIR', '.')), 'step_cache') env = cast(GithubEnv, os.environ) -github = GithubApi(env.get('GITHUB_API_URL', 'https://api.github.com'), env.get('GITHUB_TOKEN')) +github = GithubApi(env.get('GITHUB_API_URL', 'https://api.github.com'), env['GITHUB_TOKEN']) def _mask_backend_config(action_inputs: PlanPrInputs) -> Optional[str]: @@ -174,8 +174,8 @@ def get_pr() -> PrUrl: return cast(PrUrl, pr_url) def comment_hash(value: str, salt: str) -> str: - h = hashlib.sha256(f'dflook/terraform-github-actions/{salt}') - h.update(value) + h = hashlib.sha256(f'dflook/terraform-github-actions/{salt}'.encode()) + h.update(value.encode()) return h.hexdigest() def get_comment(action_inputs: PlanPrInputs, backend_fingerprint: str) -> TerraformComment: @@ -261,6 +261,7 @@ def main() -> int: f.write(comment.body) step_cache['comment'] = serialize(comment) + return 0 if __name__ == '__main__': sys.exit(main()) diff --git a/image/src/github_pr_comment/backend_config.py b/image/src/github_pr_comment/backend_config.py index a12a3bdf..fc8b7c4e 100644 --- a/image/src/github_pr_comment/backend_config.py +++ b/image/src/github_pr_comment/backend_config.py @@ -45,7 +45,7 @@ def read_backend_config_vars(init_inputs: InitInputs) -> BackendConfig: def complete_config(action_inputs: InitInputs, module: TerraformModule) -> Tuple[BackendType, BackendConfig]: backend_type, config = partial_backend_config(module) - for key, value in read_backend_config_vars(action_inputs): + for key, value in read_backend_config_vars(action_inputs).items(): config[key] = value return backend_type, config diff --git a/image/src/github_pr_comment/backend_fingerprint.py b/image/src/github_pr_comment/backend_fingerprint.py index c775901c..5f6fad3e 100644 --- a/image/src/github_pr_comment/backend_fingerprint.py +++ b/image/src/github_pr_comment/backend_fingerprint.py @@ -191,5 +191,5 @@ def fingerprint(backend_type: BackendType, backend_config: BackendConfig, env) - 'local': fingerprint_local, } - fingerprint_inputs = backends.get(backend_type, lambda c: c)(backend_config, env) + fingerprint_inputs = backends.get(backend_type, lambda c, e: c)(backend_config, env) return canonicaljson.encode_canonical_json(fingerprint_inputs) diff --git a/image/src/github_pr_comment/comment.py b/image/src/github_pr_comment/comment.py index 8c2cfacb..c67dea84 100644 --- a/image/src/github_pr_comment/comment.py +++ b/image/src/github_pr_comment/comment.py @@ -292,6 +292,6 @@ def update_comment( else: response = github.post(comment.issue_url, json={'body': _to_api_payload(new_comment)}) response.raise_for_status() - new_comment.url = response.json()['url'] + new_comment.comment_url = response.json()['url'] return new_comment From 6867f533e34c80ce1820f26e42d4b4dd19b5b379 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Fri, 18 Mar 2022 20:08:21 +0000 Subject: [PATCH 08/18] bytes --- image/src/github_pr_comment/__main__.py | 6 +++--- image/src/github_pr_comment/backend_fingerprint.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/image/src/github_pr_comment/__main__.py b/image/src/github_pr_comment/__main__.py index 23a1904f..49710d81 100644 --- a/image/src/github_pr_comment/__main__.py +++ b/image/src/github_pr_comment/__main__.py @@ -173,12 +173,12 @@ def get_pr() -> PrUrl: return cast(PrUrl, pr_url) -def comment_hash(value: str, salt: str) -> str: +def comment_hash(value: bytes, salt: str) -> str: h = hashlib.sha256(f'dflook/terraform-github-actions/{salt}'.encode()) - h.update(value.encode()) + h.update(value) return h.hexdigest() -def get_comment(action_inputs: PlanPrInputs, backend_fingerprint: str) -> TerraformComment: +def get_comment(action_inputs: PlanPrInputs, backend_fingerprint: bytes) -> TerraformComment: if 'comment' in step_cache: return deserialize(step_cache['comment']) diff --git a/image/src/github_pr_comment/backend_fingerprint.py b/image/src/github_pr_comment/backend_fingerprint.py index 5f6fad3e..f541252c 100644 --- a/image/src/github_pr_comment/backend_fingerprint.py +++ b/image/src/github_pr_comment/backend_fingerprint.py @@ -170,7 +170,7 @@ def fingerprint_local(backend_config: BackendConfig, env) -> dict[str, str]: return fingerprint_inputs -def fingerprint(backend_type: BackendType, backend_config: BackendConfig, env) -> str: +def fingerprint(backend_type: BackendType, backend_config: BackendConfig, env) -> bytes: backends = { 'remote': fingerprint_remote, 'artifactory': fingerprint_artifactory, From 35e091eedaa00bd849e14f7d969e1912f098535d Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Fri, 18 Mar 2022 23:13:49 +0000 Subject: [PATCH 09/18] Include plan modifiers in headers --- image/src/github_pr_comment/__main__.py | 13 +++++++++++++ image/src/github_pr_comment/backend_fingerprint.py | 4 ++++ image/src/github_pr_comment/comment.py | 6 ++++-- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/image/src/github_pr_comment/__main__.py b/image/src/github_pr_comment/__main__.py index 49710d81..69bc5e69 100644 --- a/image/src/github_pr_comment/__main__.py +++ b/image/src/github_pr_comment/__main__.py @@ -5,6 +5,8 @@ from pathlib import Path from typing import (NewType, Optional, cast) +import canonicaljson + from github_actions.api import GithubApi, IssueUrl, PrUrl from github_actions.cache import ActionsCache from github_actions.debug import debug @@ -199,6 +201,17 @@ def get_comment(action_inputs: PlanPrInputs, backend_fingerprint: bytes) -> Terr if label := os.environ.get('INPUT_LABEL'): headers['label'] = label + plan_modifier = {} + if target := os.environ.get('INPUT_TARGET'): + plan_modifier['target'] = sorted(t.strip() for t in target.replace(',', '\n', ).split('\n')) + + if replace := os.environ.get('INPUT_REPLACE'): + plan_modifier['replace'] = sorted(t.strip() for t in replace.replace(',', '\n', ).split('\n')) + + if plan_modifier: + debug(f'Plan modifier: {plan_modifier}') + headers['plan_modifier'] = hashlib.sha256(canonicaljson.encode_canonical_json(plan_modifier)) + return find_comment(github, issue_url, username, headers, legacy_description) def main() -> int: diff --git a/image/src/github_pr_comment/backend_fingerprint.py b/image/src/github_pr_comment/backend_fingerprint.py index f541252c..23941276 100644 --- a/image/src/github_pr_comment/backend_fingerprint.py +++ b/image/src/github_pr_comment/backend_fingerprint.py @@ -9,6 +9,7 @@ """ import canonicaljson +from github_actions.debug import debug from github_pr_comment.backend_config import BackendConfig, BackendType @@ -192,4 +193,7 @@ def fingerprint(backend_type: BackendType, backend_config: BackendConfig, env) - } fingerprint_inputs = backends.get(backend_type, lambda c, e: c)(backend_config, env) + + debug(f'Backend fingerprint includes {fingerprint_inputs.keys()}') + return canonicaljson.encode_canonical_json(fingerprint_inputs) diff --git a/image/src/github_pr_comment/comment.py b/image/src/github_pr_comment/comment.py index c67dea84..280e5293 100644 --- a/image/src/github_pr_comment/comment.py +++ b/image/src/github_pr_comment/comment.py @@ -221,6 +221,8 @@ def find_comment(github: GithubApi, issue_url: IssueUrl, username: str, headers: :param legacy_description: The description that must be present on the comment, if not headers are found. """ + debug(f"Searching for comment with {headers=}") + backup_comment = None for comment_payload in github.paged_get(issue_url): @@ -235,7 +237,7 @@ def find_comment(github: GithubApi, issue_url: IssueUrl, username: str, headers: # Match by headers only if matching_headers(comment, headers): - debug('Found comment that matches headers') + debug(f'Found comment that matches headers {comment.headers=} ') return comment debug(f"Didn't match comment with {comment.headers=}") @@ -253,7 +255,7 @@ def find_comment(github: GithubApi, issue_url: IssueUrl, username: str, headers: debug('Found comment matching legacy description') return backup_comment - debug('No matching comment exists') + debug('No existing comment exists') return TerraformComment( issue_url=issue_url, comment_url=None, From 380f13511ce038da21ead15b9f9c22e9ab7d8d20 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sun, 1 May 2022 20:04:24 +0100 Subject: [PATCH 10/18] Isolate test module per workflow --- .github/workflows/pull_request_review.yaml | 4 +- .github/workflows/pull_request_target.yaml | 4 +- .github/workflows/test-apply.yaml | 113 +++++++++-------- .github/workflows/test-changes-only.yaml | 16 +-- .github/workflows/test-check.yaml | 4 +- .../{test-remote.yaml => test-cloud.yaml} | 40 +++--- .github/workflows/test-fmt-check.yaml | 4 +- .github/workflows/test-fmt.yaml | 4 +- .github/workflows/test-http.yaml | 12 +- .github/workflows/test-new-workspace.yaml | 20 +-- .github/workflows/test-output.yaml | 2 +- .github/workflows/test-plan.yaml | 31 ++--- .github/workflows/test-registry.yaml | 10 +- .github/workflows/test-ssh.yaml | 6 +- .github/workflows/test-target-replace.yaml | 46 +++---- .github/workflows/test-validate.yaml | 10 +- .github/workflows/test-version.yaml | 58 ++++----- .github/workflows/test-workflow-commands.yaml | 2 +- .../terraform_version/test_asdf.py | 0 .../terraform_version/test_local_state.py | 0 .../terraform_version/test_remote_state_s3.py | 0 .../terraform_version/test_state.py | 0 .../test_terraform_version.py | 0 .../terraform_version/test_tfc.py | 0 .../terraform_version/test_tfenv.py | 0 .../terraform_version/test_tfswitch.py | 0 tests/{validate => }/test_validate.py | 0 tests/{version => }/test_version.py | 0 .../pull_request_review}/main.tf | 0 tests/workflows/pull_request_target/main.tf | 7 ++ .../test-apply}/apply-error/main.tf | 0 .../backend_config_12/backend_config | 0 .../test-apply}/backend_config_12/main.tf | 0 .../backend_config_13/backend_config | 0 .../test-apply}/backend_config_13/main.tf | 0 tests/workflows/test-apply/changes/main.tf | 7 ++ .../test-apply/deprecated_var}/main.tf | 0 .../test-apply}/error/main.tf | 0 .../test-apply}/no_changes/main.tf | 0 .../test-apply}/no_plan/main.tf | 0 .../test-apply}/refresh_15/main.tf | 0 .../test-apply/remote}/main.tf | 0 .../test-apply}/test.tfvars | 0 tests/workflows/test-apply/vars/main.tf | 44 +++++++ .../test-changes-only}/main.tf | 0 .../test-check/changes}/main.tf | 0 .../test-check}/no_changes/main.tf | 0 .../test-cloud}/0.13/main.tf | 0 .../test-cloud}/0.13/my_variable.tfvars | 0 .../test-cloud}/1.0/main.tf | 0 .../test-cloud}/1.0/my_variable.tfvars | 0 .../test-cloud}/1.1/main.tf | 0 .../test-cloud}/1.1/my_variable.tfvars | 0 .../test-fmt-check}/canonical/main.tf | 0 .../test-fmt-check}/canonical/subdir/main.tf | 0 .../test-fmt-check}/non-canonical/main.tf | 0 .../non-canonical/subdir/main.tf | 0 tests/workflows/test-fmt/canonical/main.tf | 4 + .../test-fmt/canonical/subdir/main.tf | 4 + .../workflows/test-fmt/non-canonical/main.tf | 10 ++ .../test-fmt/non-canonical/subdir/main.tf | 4 + .../test-http}/http-module/main.tf | 0 .../test-http}/main.tf | 0 .../test-new-workspace}/main.tf | 0 tests/workflows/test-output/main.tf | 116 ++++++++++++++++++ .../workflows/test-plan/changes-only/main.tf | 12 ++ .../test-plan}/error/main.tf | 0 tests/workflows/test-plan/no_changes/main.tf | 3 + tests/workflows/test-plan/plan/main.tf | 7 ++ .../test-plan}/plan_11/main.tf | 0 .../test-plan}/plan_12/main.tf | 0 .../test-plan}/plan_13/main.tf | 0 .../test-plan}/plan_14/main.tf | 0 .../test-plan}/plan_15/main.tf | 0 .../test-plan}/plan_15_4/main.tf | 0 .../test-registry}/main.tf | 0 .../test-registry}/test-module/README.md | 0 .../test-registry}/test-module/main.tf | 0 .../test-ssh}/main.tf | 0 .../test-target-replace}/main.tf | 0 .../test-validate}/invalid/main.tf | 0 .../test-validate}/report/error.json | 0 .../test-validate}/report/file_location.json | 0 .../test-validate}/report/line_num.json | 0 .../test-validate}/report/non_json.txt | 0 .../report/test_convert_validate_report.sh | 0 .../test-validate}/report/valid.json | 0 .../test-validate}/valid/main.tf | 0 .../test-validate}/workspace_eval/main.tf | 0 .../test-version}/asdf/.tool-versions | 0 .../test-version}/cloud/main.tf | 0 .../test-version}/empty/README.md | 0 .../test-version}/local/main.tf | 0 .../test-version}/local/terraform.tfstate | 0 .../test-version}/providers/0.11/main.tf | 0 .../test-version}/providers/0.12/main.tf | 0 .../test-version}/providers/0.13/versions.tf | 0 .../test-version}/range/main.tf | 0 .../test-version}/required_version/main.tf | 0 .../test-version}/state/main.tf | 0 .../test-version}/terraform-cloud/main.tf | 0 .../test-version}/tfenv/.terraform-version | 0 .../test-version}/tfenv/main.tf | 0 .../test-version}/tfswitch/.tfswitchrc | 0 .../test-version}/tfswitch/main.tf | 0 .../workflows/test-workflow-commands/main.tf | 7 ++ 106 files changed, 425 insertions(+), 186 deletions(-) rename .github/workflows/{test-remote.yaml => test-cloud.yaml} (82%) rename tests/{python => }/terraform_version/test_asdf.py (100%) rename tests/{python => }/terraform_version/test_local_state.py (100%) rename tests/{python => }/terraform_version/test_remote_state_s3.py (100%) rename tests/{python => }/terraform_version/test_state.py (100%) rename tests/{python => }/terraform_version/test_terraform_version.py (100%) rename tests/{python => }/terraform_version/test_tfc.py (100%) rename tests/{python => }/terraform_version/test_tfenv.py (100%) rename tests/{python => }/terraform_version/test_tfswitch.py (100%) rename tests/{validate => }/test_validate.py (100%) rename tests/{version => }/test_version.py (100%) rename tests/{apply/changes => workflows/pull_request_review}/main.tf (100%) create mode 100644 tests/workflows/pull_request_target/main.tf rename tests/{apply => workflows/test-apply}/apply-error/main.tf (100%) rename tests/{apply => workflows/test-apply}/backend_config_12/backend_config (100%) rename tests/{apply => workflows/test-apply}/backend_config_12/main.tf (100%) rename tests/{apply => workflows/test-apply}/backend_config_13/backend_config (100%) rename tests/{apply => workflows/test-apply}/backend_config_13/main.tf (100%) create mode 100644 tests/workflows/test-apply/changes/main.tf rename tests/{apply/vars => workflows/test-apply/deprecated_var}/main.tf (100%) rename tests/{apply => workflows/test-apply}/error/main.tf (100%) rename tests/{apply => workflows/test-apply}/no_changes/main.tf (100%) rename tests/{apply => workflows/test-apply}/no_plan/main.tf (100%) rename tests/{apply => workflows/test-apply}/refresh_15/main.tf (100%) rename tests/{remote-state/test-bucket_12 => workflows/test-apply/remote}/main.tf (100%) rename tests/{apply => workflows/test-apply}/test.tfvars (100%) create mode 100644 tests/workflows/test-apply/vars/main.tf rename tests/{plan/changes-only => workflows/test-changes-only}/main.tf (100%) rename tests/{plan/plan => workflows/test-check/changes}/main.tf (100%) rename tests/{plan => workflows/test-check}/no_changes/main.tf (100%) rename tests/{terraform-cloud => workflows/test-cloud}/0.13/main.tf (100%) rename tests/{terraform-cloud => workflows/test-cloud}/0.13/my_variable.tfvars (100%) rename tests/{terraform-cloud => workflows/test-cloud}/1.0/main.tf (100%) rename tests/{terraform-cloud => workflows/test-cloud}/1.0/my_variable.tfvars (100%) rename tests/{terraform-cloud => workflows/test-cloud}/1.1/main.tf (100%) rename tests/{terraform-cloud => workflows/test-cloud}/1.1/my_variable.tfvars (100%) rename tests/{fmt => workflows/test-fmt-check}/canonical/main.tf (100%) rename tests/{fmt => workflows/test-fmt-check}/canonical/subdir/main.tf (100%) rename tests/{fmt => workflows/test-fmt-check}/non-canonical/main.tf (100%) rename tests/{fmt => workflows/test-fmt-check}/non-canonical/subdir/main.tf (100%) create mode 100644 tests/workflows/test-fmt/canonical/main.tf create mode 100644 tests/workflows/test-fmt/canonical/subdir/main.tf create mode 100644 tests/workflows/test-fmt/non-canonical/main.tf create mode 100644 tests/workflows/test-fmt/non-canonical/subdir/main.tf rename tests/{ => workflows/test-http}/http-module/main.tf (100%) rename tests/{git-http-module => workflows/test-http}/main.tf (100%) rename tests/{new-workspace => workflows/test-new-workspace}/main.tf (100%) create mode 100644 tests/workflows/test-output/main.tf create mode 100644 tests/workflows/test-plan/changes-only/main.tf rename tests/{plan => workflows/test-plan}/error/main.tf (100%) create mode 100644 tests/workflows/test-plan/no_changes/main.tf create mode 100644 tests/workflows/test-plan/plan/main.tf rename tests/{plan => workflows/test-plan}/plan_11/main.tf (100%) rename tests/{plan => workflows/test-plan}/plan_12/main.tf (100%) rename tests/{plan => workflows/test-plan}/plan_13/main.tf (100%) rename tests/{plan => workflows/test-plan}/plan_14/main.tf (100%) rename tests/{plan => workflows/test-plan}/plan_15/main.tf (100%) rename tests/{plan => workflows/test-plan}/plan_15_4/main.tf (100%) rename tests/{registry => workflows/test-registry}/main.tf (100%) rename tests/{registry => workflows/test-registry}/test-module/README.md (100%) rename tests/{registry => workflows/test-registry}/test-module/main.tf (100%) rename tests/{ssh-module => workflows/test-ssh}/main.tf (100%) rename tests/{target => workflows/test-target-replace}/main.tf (100%) rename tests/{validate => workflows/test-validate}/invalid/main.tf (100%) rename tests/{validate => workflows/test-validate}/report/error.json (100%) rename tests/{validate => workflows/test-validate}/report/file_location.json (100%) rename tests/{validate => workflows/test-validate}/report/line_num.json (100%) rename tests/{validate => workflows/test-validate}/report/non_json.txt (100%) rename tests/{validate => workflows/test-validate}/report/test_convert_validate_report.sh (100%) rename tests/{validate => workflows/test-validate}/report/valid.json (100%) rename tests/{validate => workflows/test-validate}/valid/main.tf (100%) rename tests/{validate => workflows/test-validate}/workspace_eval/main.tf (100%) rename tests/{version => workflows/test-version}/asdf/.tool-versions (100%) rename tests/{version => workflows/test-version}/cloud/main.tf (100%) rename tests/{version => workflows/test-version}/empty/README.md (100%) rename tests/{version => workflows/test-version}/local/main.tf (100%) rename tests/{version => workflows/test-version}/local/terraform.tfstate (100%) rename tests/{version => workflows/test-version}/providers/0.11/main.tf (100%) rename tests/{version => workflows/test-version}/providers/0.12/main.tf (100%) rename tests/{version => workflows/test-version}/providers/0.13/versions.tf (100%) rename tests/{version => workflows/test-version}/range/main.tf (100%) rename tests/{version => workflows/test-version}/required_version/main.tf (100%) rename tests/{version => workflows/test-version}/state/main.tf (100%) rename tests/{version => workflows/test-version}/terraform-cloud/main.tf (100%) rename tests/{version => workflows/test-version}/tfenv/.terraform-version (100%) rename tests/{version => workflows/test-version}/tfenv/main.tf (100%) rename tests/{version => workflows/test-version}/tfswitch/.tfswitchrc (100%) rename tests/{version => workflows/test-version}/tfswitch/main.tf (100%) create mode 100644 tests/workflows/test-workflow-commands/main.tf diff --git a/.github/workflows/pull_request_review.yaml b/.github/workflows/pull_request_review.yaml index 4ad1f859..2fa0f4f9 100644 --- a/.github/workflows/pull_request_review.yaml +++ b/.github/workflows/pull_request_review.yaml @@ -17,14 +17,14 @@ jobs: uses: ./terraform-plan with: label: pull_request_review - path: tests/apply/changes + path: tests/workflows/pull_request_review - name: Apply uses: ./terraform-apply id: output with: label: pull_request_review - path: tests/apply/changes + path: tests/workflows/pull_request_review - name: Verify outputs run: | diff --git a/.github/workflows/pull_request_target.yaml b/.github/workflows/pull_request_target.yaml index 7267a409..885938f0 100644 --- a/.github/workflows/pull_request_target.yaml +++ b/.github/workflows/pull_request_target.yaml @@ -17,14 +17,14 @@ jobs: uses: ./terraform-plan with: label: pull_request_target - path: tests/apply/changes + path: tests/workflows/pull_request_target - name: Apply uses: ./terraform-apply id: output with: label: pull_request_target - path: tests/apply/changes + path: tests/workflows/pull_request_target - name: Verify outputs run: | diff --git a/.github/workflows/test-apply.yaml b/.github/workflows/test-apply.yaml index 7b2f13bf..7d17eda8 100644 --- a/.github/workflows/test-apply.yaml +++ b/.github/workflows/test-apply.yaml @@ -18,7 +18,7 @@ jobs: uses: ./terraform-apply id: output with: - path: tests/remote-state/test-bucket_12 + path: tests/workflows/test-apply/remote auto_approve: true - name: Verify outputs @@ -50,7 +50,7 @@ jobs: id: apply continue-on-error: true with: - path: tests/remote-state/test-bucket_12 + path: tests/workflows/test-apply/remote auto_approve: true - name: Check failed to apply @@ -89,16 +89,16 @@ jobs: - name: Plan uses: ./terraform-plan with: - label: Apply Error - path: tests/apply/apply-error + label: test-apply apply_apply_error + path: tests/workflows/test-apply/apply-error - name: Apply uses: ./terraform-apply id: apply continue-on-error: true with: - label: Apply Error - path: tests/apply/apply-error + label: test-apply apply_apply_error + path: tests/workflows/test-apply/apply-error - name: Check failed to apply run: | @@ -134,7 +134,7 @@ jobs: id: apply continue-on-error: true with: - path: tests/apply/changes + path: tests/workflows/test-apply/changes - name: Check failed to apply run: | @@ -160,13 +160,15 @@ jobs: - name: Plan uses: ./terraform-plan with: - path: tests/apply/changes + label: test-apply apply + path: tests/workflows/test-apply/changes - name: Apply uses: ./terraform-apply id: first-apply with: - path: tests/apply/changes + label: test-apply apply + path: tests/workflows/test-apply/changes - name: Verify outputs run: | @@ -189,7 +191,8 @@ jobs: uses: ./terraform-apply id: second-apply with: - path: tests/apply/changes + label: test-apply apply + path: tests/workflows/test-apply/changes - name: Verify outputs run: | @@ -220,7 +223,8 @@ jobs: - name: Plan uses: ./terraform-plan with: - path: tests/apply/vars + label: test-apply apply_variables + path: tests/workflows/test-apply/vars variables: | my_var="hello" complex_input=[ @@ -235,13 +239,14 @@ jobs: protocol = "tcp" }, ] - var_file: tests/apply/test.tfvars + var_file: tests/workflows/test-apply/test.tfvars - name: Apply uses: ./terraform-apply id: output with: - path: tests/apply/vars + label: test-apply apply_variables + path: tests/workflows/test-apply/vars variables: | my_var="hello" complex_input=[ @@ -256,7 +261,7 @@ jobs: protocol = "tcp" }, ] - var_file: tests/apply/test.tfvars + var_file: tests/workflows/test-apply/test.tfvars - name: Verify outputs run: | @@ -304,15 +309,17 @@ jobs: - name: Plan uses: ./terraform-plan with: - path: tests/apply/backend_config_12 - backend_config_file: tests/apply/backend_config_12/backend_config + label: test-apply backend_config_12 backend_config_file + path: tests/workflows/test-apply/backend_config_12 + backend_config_file: tests/workflows/test-apply/backend_config_12/backend_config - name: Apply uses: ./terraform-apply id: backend_config_file_12 with: - path: tests/apply/backend_config_12 - backend_config_file: tests/apply/backend_config_12/backend_config + label: test-apply backend_config_12 backend_config_file + path: tests/workflows/test-apply/backend_config_12 + backend_config_file: tests/workflows/test-apply/backend_config_12/backend_config - name: Verify outputs run: | @@ -334,7 +341,8 @@ jobs: - name: Plan uses: ./terraform-plan with: - path: tests/apply/backend_config_12 + label: test-apply backend_config_12 backend_config + path: tests/workflows/test-apply/backend_config_12 backend_config: | bucket=terraform-github-actions key=backend_config @@ -344,7 +352,8 @@ jobs: uses: ./terraform-apply id: backend_config_12 with: - path: tests/apply/backend_config_12 + label: test-apply backend_config_12 backend_config + path: tests/workflows/test-apply/backend_config_12 backend_config: | bucket=terraform-github-actions key=backend_config @@ -381,15 +390,17 @@ jobs: - name: Plan uses: ./terraform-plan with: - path: tests/apply/backend_config_13 - backend_config_file: tests/apply/backend_config_13/backend_config + label: test-apply backend_config_12 backend_config_file + path: tests/workflows/test-apply/backend_config_13 + backend_config_file: tests/workflows/test-apply/backend_config_13/backend_config - name: Apply uses: ./terraform-apply id: backend_config_file_13 with: - path: tests/apply/backend_config_13 - backend_config_file: tests/apply/backend_config_13/backend_config + label: test-apply backend_config_12 backend_config_file + path: tests/workflows/test-apply/backend_config_13 + backend_config_file: tests/workflows/test-apply/backend_config_13/backend_config - name: Verify outputs run: | @@ -411,7 +422,8 @@ jobs: - name: Plan uses: ./terraform-plan with: - path: tests/apply/backend_config_13 + label: test-apply backend_config_12 backend_config + path: tests/workflows/test-apply/backend_config_13 backend_config: | bucket=terraform-github-actions key=backend_config_13 @@ -421,7 +433,8 @@ jobs: uses: ./terraform-apply id: backend_config_13 with: - path: tests/apply/backend_config_13 + label: test-apply backend_config_12 backend_config + path: tests/workflows/test-apply/backend_config_13 backend_config: | bucket=terraform-github-actions key=backend_config_13 @@ -456,19 +469,19 @@ jobs: - name: Plan uses: ./terraform-plan with: - path: tests/apply/vars + path: tests/workflows/test-apply/vars label: TestLabel variables: my_var="world" - var_file: tests/apply/test.tfvars + var_file: tests/workflows/test-apply/test.tfvars - name: Apply uses: ./terraform-apply id: output with: - path: tests/apply/vars + path: tests/workflows/test-apply/vars label: TestLabel variables: my_var="world" - var_file: tests/apply/test.tfvars + var_file: tests/workflows/test-apply/test.tfvars - name: Verify outputs run: | @@ -502,7 +515,7 @@ jobs: uses: ./terraform-apply id: output with: - path: tests/remote-state/test-bucket_12 + path: tests/workflows/test-apply/remote - name: Verify outputs run: | @@ -536,7 +549,7 @@ jobs: id: apply continue-on-error: true with: - path: tests/apply/no_plan + path: tests/workflows/test-apply/no_plan - name: Check failed to apply run: | @@ -568,14 +581,14 @@ jobs: uses: ./terraform-plan with: label: User PAT - path: tests/apply/changes + path: tests/workflows/test-apply/changes - name: Apply uses: ./terraform-apply id: output with: label: User PAT - path: tests/apply/changes + path: tests/workflows/test-apply/changes - name: Verify outputs run: | @@ -606,17 +619,17 @@ jobs: - name: Plan uses: ./terraform-plan with: - path: tests/apply/vars + path: tests/workflows/test-apply/deprecated_var var: my_var=hello - var_file: tests/apply/test.tfvars + var_file: tests/workflows/test-apply/test.tfvars - name: Apply uses: ./terraform-apply id: output with: - path: tests/apply/vars + path: tests/workflows/test-apply/deprecated_var var: my_var=hello - var_file: tests/apply/test.tfvars + var_file: tests/workflows/test-apply/test.tfvars - name: Verify outputs run: | @@ -645,30 +658,30 @@ jobs: - name: Plan 1 uses: ./terraform-plan with: - label: Refresh 1 - path: tests/apply/refresh_15 + label: test-apply apply_refresh 1 + path: tests/workflows/test-apply/refresh_15 variables: len=10 - name: Apply 1 uses: ./terraform-apply with: - label: Refresh 1 - path: tests/apply/refresh_15 + label: test-apply apply_refresh 1 + path: tests/workflows/test-apply/refresh_15 variables: len=10 - name: Plan 2 uses: ./terraform-plan with: - label: Refresh 2 - path: tests/apply/refresh_15 + label: test-apply apply_refresh 2 + path: tests/workflows/test-apply/refresh_15 variables: len=20 - name: Apply 2 uses: ./terraform-apply id: output with: - label: Refresh 2 - path: tests/apply/refresh_15 + label: test-apply apply_refresh 2 + path: tests/workflows/test-apply/refresh_15 variables: len=20 apply_with_pre_run: @@ -686,15 +699,15 @@ jobs: - name: Plan uses: ./terraform-plan with: - label: pre-run - path: tests/apply/changes + label: test-apply apply_with_pre_run + path: tests/workflows/test-apply/changes - name: Apply uses: ./terraform-apply id: output with: - label: pre-run - path: tests/apply/changes + label: test-apply apply_with_pre_run + path: tests/workflows/test-apply/changes - name: Verify outputs run: | diff --git a/.github/workflows/test-changes-only.yaml b/.github/workflows/test-changes-only.yaml index 0a725d28..151d7a4e 100644 --- a/.github/workflows/test-changes-only.yaml +++ b/.github/workflows/test-changes-only.yaml @@ -35,7 +35,7 @@ jobs: id: apply with: label: no_changes - path: tests/plan/changes-only + path: tests/workflows/test-changes-only - name: Check failure-reason run: | @@ -58,7 +58,7 @@ jobs: id: changes-plan with: label: change_then_no_changes - path: tests/plan/changes-only + path: tests/workflows/test-changes-only variables: | cause-changes=true add_github_comment: changes-only @@ -77,7 +77,7 @@ jobs: id: plan with: label: change_then_no_changes - path: tests/plan/changes-only + path: tests/workflows/test-changes-only variables: | cause-changes=false add_github_comment: changes-only @@ -96,7 +96,7 @@ jobs: id: apply with: label: change_then_no_changes - path: tests/plan/changes-only + path: tests/workflows/test-changes-only variables: | cause-changes=false @@ -120,7 +120,7 @@ jobs: uses: ./terraform-plan id: plan with: - path: tests/plan/changes-only + path: tests/workflows/test-changes-only label: no_changes_then_changes variables: | cause-changes=false @@ -140,7 +140,7 @@ jobs: id: apply continue-on-error: true with: - path: tests/plan/changes-only + path: tests/workflows/test-changes-only label: no_changes_then_changes variables: | cause-changes=true @@ -169,7 +169,7 @@ jobs: - name: Plan Changes uses: ./terraform-plan with: - path: tests/plan/changes-only + path: tests/workflows/test-changes-only label: apply_when_plan_has_changed variables: | cause-changes=true @@ -179,7 +179,7 @@ jobs: id: apply continue-on-error: true with: - path: tests/plan/changes-only + path: tests/workflows/test-changes-only label: apply_when_plan_has_changed variables: | cause-changes=true diff --git a/.github/workflows/test-check.yaml b/.github/workflows/test-check.yaml index 6afd9ff2..33ec7fd2 100644 --- a/.github/workflows/test-check.yaml +++ b/.github/workflows/test-check.yaml @@ -15,7 +15,7 @@ jobs: uses: ./terraform-check id: check with: - path: tests/plan/no_changes + path: tests/workflows/test-check/no_changes - name: Check failure-reason run: | @@ -36,7 +36,7 @@ jobs: continue-on-error: true id: check with: - path: tests/plan/plan + path: tests/workflows/test-check/changes - name: Check failure-reason run: | diff --git a/.github/workflows/test-remote.yaml b/.github/workflows/test-cloud.yaml similarity index 82% rename from .github/workflows/test-remote.yaml rename to .github/workflows/test-cloud.yaml index 6efff610..6427cbf3 100644 --- a/.github/workflows/test-remote.yaml +++ b/.github/workflows/test-cloud.yaml @@ -1,4 +1,4 @@ -name: Test remote backend +name: Test Terraform cloud on: - pull_request @@ -18,21 +18,21 @@ jobs: - name: Create a new workspace with no existing workspaces uses: ./terraform-new-workspace with: - path: tests/terraform-cloud/${{ matrix.tf_version }} + path: tests/workflows/terraform-cloud/${{ matrix.tf_version }} workspace: ${{ github.head_ref }}-1 backend_config: token=${{ secrets.TF_API_TOKEN }} - name: Create a new workspace when it doesn't exist uses: ./terraform-new-workspace with: - path: tests/terraform-cloud/${{ matrix.tf_version }} + path: tests/workflows/terraform-cloud/${{ matrix.tf_version }} workspace: ${{ github.head_ref }}-2 backend_config: token=${{ secrets.TF_API_TOKEN }} - name: Create a new workspace when it already exists uses: ./terraform-new-workspace with: - path: tests/terraform-cloud/${{ matrix.tf_version }} + path: tests/workflows/terraform-cloud/${{ matrix.tf_version }} workspace: ${{ github.head_ref }}-2 backend_config: token=${{ secrets.TF_API_TOKEN }} @@ -40,12 +40,12 @@ jobs: uses: ./terraform-apply id: auto_apply with: - path: tests/terraform-cloud/${{ matrix.tf_version }} + path: tests/workflows/terraform-cloud/${{ matrix.tf_version }} workspace: ${{ github.head_ref }}-1 backend_config: token=${{ secrets.TF_API_TOKEN }} auto_approve: true var_file: | - tests/terraform-cloud/${{ matrix.tf_version }}/my_variable.tfvars + tests/workflows/terraform-cloud/${{ matrix.tf_version }}/my_variable.tfvars variables: | from_variables="from_variables" @@ -80,7 +80,7 @@ jobs: uses: ./terraform-output id: output with: - path: tests/terraform-cloud/${{ matrix.tf_version }} + path: tests/workflows/terraform-cloud/${{ matrix.tf_version }} workspace: ${{ github.head_ref }}-1 backend_config: token=${{ secrets.TF_API_TOKEN }} @@ -95,9 +95,9 @@ jobs: run: | mkdir fixed-workspace-name if [[ "${{ matrix.tf_version }}" == "0.13" ]]; then - sed -e 's/prefix.*/name = "github-actions-0-13-${{ github.head_ref }}-1"/' tests/terraform-cloud/${{ matrix.tf_version }}/main.tf > fixed-workspace-name/main.tf + sed -e 's/prefix.*/name = "github-actions-0-13-${{ github.head_ref }}-1"/' tests/workflows/terraform-cloud/${{ matrix.tf_version }}/main.tf > fixed-workspace-name/main.tf else - sed -e 's/prefix.*/name = "github-actions-1-1-${{ github.head_ref }}-1"/' tests/terraform-cloud/${{ matrix.tf_version }}/main.tf > fixed-workspace-name/main.tf + sed -e 's/prefix.*/name = "github-actions-1-1-${{ github.head_ref }}-1"/' tests/workflows/terraform-cloud/${{ matrix.tf_version }}/main.tf > fixed-workspace-name/main.tf fi - name: Get outputs @@ -117,11 +117,11 @@ jobs: - name: Check no changes uses: ./terraform-check with: - path: tests/terraform-cloud/${{ matrix.tf_version }} + path: tests/workflows/terraform-cloud/${{ matrix.tf_version }} workspace: ${{ github.head_ref }}-1 backend_config: token=${{ secrets.TF_API_TOKEN }} var_file: | - tests/terraform-cloud/${{ matrix.tf_version }}/my_variable.tfvars + tests/workflows/terraform-cloud/${{ matrix.tf_version }}/my_variable.tfvars variables: | from_variables="from_variables" @@ -130,11 +130,11 @@ jobs: id: check continue-on-error: true with: - path: tests/terraform-cloud/${{ matrix.tf_version }} + path: tests/workflows/terraform-cloud/${{ matrix.tf_version }} workspace: ${{ github.head_ref }}-1 backend_config: token=${{ secrets.TF_API_TOKEN }} var_file: | - tests/terraform-cloud/${{ matrix.tf_version }}/my_variable.tfvars + tests/workflows/terraform-cloud/${{ matrix.tf_version }}/my_variable.tfvars variables: | from_variables="Changed!" @@ -153,7 +153,7 @@ jobs: - name: Destroy workspace uses: ./terraform-destroy-workspace with: - path: tests/terraform-cloud/${{ matrix.tf_version }} + path: tests/workflows/terraform-cloud/${{ matrix.tf_version }} workspace: ${{ github.head_ref }}-1 backend_config: token=${{ secrets.TF_API_TOKEN }} @@ -163,11 +163,11 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - path: tests/terraform-cloud/${{ matrix.tf_version }} + path: tests/workflows/terraform-cloud/${{ matrix.tf_version }} workspace: ${{ github.head_ref }}-2 backend_config: token=${{ secrets.TF_API_TOKEN }} var_file: | - tests/terraform-cloud/${{ matrix.tf_version }}/my_variable.tfvars + tests/workflows/terraform-cloud/${{ matrix.tf_version }}/my_variable.tfvars variables: | from_variables="from_variables" @@ -189,11 +189,11 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - path: tests/terraform-cloud/${{ matrix.tf_version }} + path: tests/workflows/terraform-cloud/${{ matrix.tf_version }} workspace: ${{ github.head_ref }}-2 backend_config: token=${{ secrets.TF_API_TOKEN }} var_file: | - tests/terraform-cloud/${{ matrix.tf_version }}/my_variable.tfvars + tests/workflows/terraform-cloud/${{ matrix.tf_version }}/my_variable.tfvars variables: | from_variables="from_variables" @@ -227,7 +227,7 @@ jobs: - name: Destroy the last workspace uses: ./terraform-destroy-workspace with: - path: tests/terraform-cloud/${{ matrix.tf_version }} + path: tests/workflows/terraform-cloud/${{ matrix.tf_version }} workspace: ${{ github.head_ref }}-2 backend_config: token=${{ secrets.TF_API_TOKEN }} @@ -236,7 +236,7 @@ jobs: continue-on-error: true id: destroy-non-existant-workspace with: - path: tests/terraform-cloud/${{ matrix.tf_version }} + path: tests/workflows/terraform-cloud/${{ matrix.tf_version }} workspace: ${{ github.head_ref }}-1 backend_config: token=${{ secrets.TF_API_TOKEN }} - name: Check failed to destroy diff --git a/.github/workflows/test-fmt-check.yaml b/.github/workflows/test-fmt-check.yaml index 05186fee..f316521d 100644 --- a/.github/workflows/test-fmt-check.yaml +++ b/.github/workflows/test-fmt-check.yaml @@ -15,7 +15,7 @@ jobs: uses: ./terraform-fmt-check id: fmt-check with: - path: tests/fmt/canonical + path: tests/workflows/test-fmt-check/canonical - name: Check valid run: | @@ -37,7 +37,7 @@ jobs: continue-on-error: true id: fmt-check with: - path: tests/fmt/non-canonical + path: tests/workflows/test-fmt-check/non-canonical - name: Check invalid run: | diff --git a/.github/workflows/test-fmt.yaml b/.github/workflows/test-fmt.yaml index 56c24800..760d210d 100644 --- a/.github/workflows/test-fmt.yaml +++ b/.github/workflows/test-fmt.yaml @@ -14,9 +14,9 @@ jobs: - name: terraform fmt uses: ./terraform-fmt with: - path: tests/fmt/non-canonical + path: tests/workflows/test-fmt/non-canonical - name: fmt-check uses: ./terraform-fmt-check with: - path: tests/fmt/non-canonical + path: tests/workflows/test-fmt/non-canonical diff --git a/.github/workflows/test-http.yaml b/.github/workflows/test-http.yaml index cafca1b7..8e231ce8 100644 --- a/.github/workflows/test-http.yaml +++ b/.github/workflows/test-http.yaml @@ -23,7 +23,7 @@ jobs: uses: ./terraform-apply id: output with: - path: tests/git-http-module + path: tests/workflows/test-http auto_approve: true - name: Verify outputs @@ -49,7 +49,7 @@ jobs: uses: ./terraform-apply id: output with: - path: tests/git-http-module + path: tests/workflows/test-http auto_approve: true - name: Verify outputs @@ -75,7 +75,7 @@ jobs: uses: ./terraform-apply id: output with: - path: tests/git-http-module + path: tests/workflows/test-http auto_approve: true - name: Verify outputs @@ -97,7 +97,7 @@ jobs: continue-on-error: true id: apply with: - path: tests/git-http-module + path: tests/workflows/test-http auto_approve: true - name: Check failed @@ -121,7 +121,7 @@ jobs: uses: ./terraform-apply id: output with: - path: tests/http-module + path: tests/workflows/test-http/http-module auto_approve: true - name: Verify outputs @@ -143,7 +143,7 @@ jobs: continue-on-error: true id: apply with: - path: tests/http-module + path: tests/workflows/test-http/http-module auto_approve: true - name: Check failed diff --git a/.github/workflows/test-new-workspace.yaml b/.github/workflows/test-new-workspace.yaml index 66375641..64959cd2 100644 --- a/.github/workflows/test-new-workspace.yaml +++ b/.github/workflows/test-new-workspace.yaml @@ -20,7 +20,7 @@ jobs: - name: Setup remote backend run: | - cat >tests/new-workspace/backend.tf <tests/workflows/test-new-workspace/backend.tf <tests/target/backend.tf <tests/workflows/test-target-replace/backend.tf <=0.12.0,<=0.12.5" with: - path: tests/version/empty + path: tests/workflows/test-version/empty - name: Print the version run: echo "The terraform version was ${{ steps.terraform-version.outputs.terraform }}" @@ -156,7 +156,7 @@ jobs: env: TERRAFORM_VERSION: 0.12.13 with: - path: tests/version/terraform-cloud + path: tests/workflows/test-version/terraform-cloud workspace: test-1 backend_config: token=${{ secrets.TF_API_TOKEN }} @@ -164,14 +164,14 @@ jobs: uses: ./terraform-version id: terraform-version with: - path: tests/version/terraform-cloud + path: tests/workflows/test-version/terraform-cloud workspace: test-1 backend_config: token=${{ secrets.TF_API_TOKEN }} - name: Destroy workspace uses: ./terraform-destroy-workspace with: - path: tests/version/terraform-cloud + path: tests/workflows/test-version/terraform-cloud workspace: test-1 backend_config: token=${{ secrets.TF_API_TOKEN }} @@ -197,7 +197,7 @@ jobs: TERRAFORM_VERSION: 1.1.2 TERRAFORM_CLOUD_TOKENS: app.terraform.io=${{ secrets.TF_API_TOKEN }} with: - path: tests/version/cloud + path: tests/workflows/test-version/cloud workspace: tfc_cloud_workspace-1 - name: Test terraform-version @@ -206,7 +206,7 @@ jobs: env: TERRAFORM_CLOUD_TOKENS: app.terraform.io=${{ secrets.TF_API_TOKEN }} with: - path: tests/version/cloud + path: tests/workflows/test-version/cloud workspace: tfc_cloud_workspace-1 - name: Destroy workspace @@ -214,7 +214,7 @@ jobs: env: TERRAFORM_CLOUD_TOKENS: app.terraform.io=${{ secrets.TF_API_TOKEN }} with: - path: tests/version/cloud + path: tests/workflows/test-version/cloud workspace: tfc_cloud_workspace-1 - name: Print the version @@ -237,7 +237,7 @@ jobs: uses: ./terraform-version id: terraform-version with: - path: tests/version/local + path: tests/workflows/test-version/local - name: Print the version run: | @@ -262,19 +262,19 @@ jobs: TERRAFORM_VERSION: 0.12.9 with: variables: my_variable="hello" - path: tests/version/state + path: tests/workflows/test-version/state auto_approve: true - name: Test terraform-version uses: ./terraform-version id: terraform-version with: - path: tests/version/state + path: tests/workflows/test-version/state - name: Destroy default workspace uses: ./terraform-destroy with: - path: tests/version/state + path: tests/workflows/test-version/state variables: my_variable="goodbye" - name: Print the version @@ -291,14 +291,14 @@ jobs: env: TERRAFORM_VERSION: 1.1.0 with: - path: tests/version/state + path: tests/workflows/test-version/state workspace: second - name: Apply second workspace uses: ./terraform-apply with: variables: my_variable="goodbye" - path: tests/version/state + path: tests/workflows/test-version/state workspace: second auto_approve: true @@ -306,13 +306,13 @@ jobs: uses: ./terraform-version id: terraform-version-second with: - path: tests/version/state + path: tests/workflows/test-version/state workspace: second - name: Destroy second workspace uses: ./terraform-destroy-workspace with: - path: tests/version/state + path: tests/workflows/test-version/state workspace: second variables: my_variable="goodbye" @@ -330,14 +330,14 @@ jobs: env: TERRAFORM_VERSION: 0.13.0 with: - path: tests/version/state + path: tests/workflows/test-version/state workspace: third - name: Apply third workspace uses: ./terraform-apply with: variables: my_variable="goodbye" - path: tests/version/state + path: tests/workflows/test-version/state workspace: third auto_approve: true @@ -345,13 +345,13 @@ jobs: uses: ./terraform-version id: terraform-version-third with: - path: tests/version/state + path: tests/workflows/test-version/state workspace: third - name: Destroy third workspace uses: ./terraform-destroy-workspace with: - path: tests/version/state + path: tests/workflows/test-version/state workspace: third variables: my_variable="goodbye" @@ -368,7 +368,7 @@ jobs: uses: ./terraform-version id: terraform-version-fourth with: - path: tests/version/state + path: tests/workflows/test-version/state workspace: fourth - name: Print the version @@ -391,7 +391,7 @@ jobs: uses: ./terraform-version id: terraform-version with: - path: tests/version/empty + path: tests/workflows/test-version/empty - name: Print the version run: echo "The terraform version was ${{ steps.terraform-version.outputs.terraform }}" @@ -414,7 +414,7 @@ jobs: uses: ./terraform-version id: terraform-version-12 with: - path: tests/version/providers/0.12 + path: tests/workflows/test-version/providers/0.12 - name: Print the version run: | @@ -438,7 +438,7 @@ jobs: uses: ./terraform-version id: terraform-version-13 with: - path: tests/version/providers/0.13 + path: tests/workflows/test-version/providers/0.13 - name: Print the version run: | @@ -462,7 +462,7 @@ jobs: uses: ./terraform-version id: terraform-version-11 with: - path: tests/version/providers/0.11 + path: tests/workflows/test-version/providers/0.11 - name: Print the version run: | diff --git a/.github/workflows/test-workflow-commands.yaml b/.github/workflows/test-workflow-commands.yaml index 87c752b4..67860efb 100644 --- a/.github/workflows/test-workflow-commands.yaml +++ b/.github/workflows/test-workflow-commands.yaml @@ -15,7 +15,7 @@ jobs: uses: ./terraform-plan id: plan with: - path: tests/plan/plan + path: tests/workflows/test-workflow-commands add_github_comment: false env: TERRAFORM_PRE_RUN: | diff --git a/tests/python/terraform_version/test_asdf.py b/tests/terraform_version/test_asdf.py similarity index 100% rename from tests/python/terraform_version/test_asdf.py rename to tests/terraform_version/test_asdf.py diff --git a/tests/python/terraform_version/test_local_state.py b/tests/terraform_version/test_local_state.py similarity index 100% rename from tests/python/terraform_version/test_local_state.py rename to tests/terraform_version/test_local_state.py diff --git a/tests/python/terraform_version/test_remote_state_s3.py b/tests/terraform_version/test_remote_state_s3.py similarity index 100% rename from tests/python/terraform_version/test_remote_state_s3.py rename to tests/terraform_version/test_remote_state_s3.py diff --git a/tests/python/terraform_version/test_state.py b/tests/terraform_version/test_state.py similarity index 100% rename from tests/python/terraform_version/test_state.py rename to tests/terraform_version/test_state.py diff --git a/tests/python/terraform_version/test_terraform_version.py b/tests/terraform_version/test_terraform_version.py similarity index 100% rename from tests/python/terraform_version/test_terraform_version.py rename to tests/terraform_version/test_terraform_version.py diff --git a/tests/python/terraform_version/test_tfc.py b/tests/terraform_version/test_tfc.py similarity index 100% rename from tests/python/terraform_version/test_tfc.py rename to tests/terraform_version/test_tfc.py diff --git a/tests/python/terraform_version/test_tfenv.py b/tests/terraform_version/test_tfenv.py similarity index 100% rename from tests/python/terraform_version/test_tfenv.py rename to tests/terraform_version/test_tfenv.py diff --git a/tests/python/terraform_version/test_tfswitch.py b/tests/terraform_version/test_tfswitch.py similarity index 100% rename from tests/python/terraform_version/test_tfswitch.py rename to tests/terraform_version/test_tfswitch.py diff --git a/tests/validate/test_validate.py b/tests/test_validate.py similarity index 100% rename from tests/validate/test_validate.py rename to tests/test_validate.py diff --git a/tests/version/test_version.py b/tests/test_version.py similarity index 100% rename from tests/version/test_version.py rename to tests/test_version.py diff --git a/tests/apply/changes/main.tf b/tests/workflows/pull_request_review/main.tf similarity index 100% rename from tests/apply/changes/main.tf rename to tests/workflows/pull_request_review/main.tf diff --git a/tests/workflows/pull_request_target/main.tf b/tests/workflows/pull_request_target/main.tf new file mode 100644 index 00000000..615bfe89 --- /dev/null +++ b/tests/workflows/pull_request_target/main.tf @@ -0,0 +1,7 @@ +resource "random_string" "my_string" { + length = 11 +} + +output "output_string" { + value = "the_string" +} diff --git a/tests/apply/apply-error/main.tf b/tests/workflows/test-apply/apply-error/main.tf similarity index 100% rename from tests/apply/apply-error/main.tf rename to tests/workflows/test-apply/apply-error/main.tf diff --git a/tests/apply/backend_config_12/backend_config b/tests/workflows/test-apply/backend_config_12/backend_config similarity index 100% rename from tests/apply/backend_config_12/backend_config rename to tests/workflows/test-apply/backend_config_12/backend_config diff --git a/tests/apply/backend_config_12/main.tf b/tests/workflows/test-apply/backend_config_12/main.tf similarity index 100% rename from tests/apply/backend_config_12/main.tf rename to tests/workflows/test-apply/backend_config_12/main.tf diff --git a/tests/apply/backend_config_13/backend_config b/tests/workflows/test-apply/backend_config_13/backend_config similarity index 100% rename from tests/apply/backend_config_13/backend_config rename to tests/workflows/test-apply/backend_config_13/backend_config diff --git a/tests/apply/backend_config_13/main.tf b/tests/workflows/test-apply/backend_config_13/main.tf similarity index 100% rename from tests/apply/backend_config_13/main.tf rename to tests/workflows/test-apply/backend_config_13/main.tf diff --git a/tests/workflows/test-apply/changes/main.tf b/tests/workflows/test-apply/changes/main.tf new file mode 100644 index 00000000..615bfe89 --- /dev/null +++ b/tests/workflows/test-apply/changes/main.tf @@ -0,0 +1,7 @@ +resource "random_string" "my_string" { + length = 11 +} + +output "output_string" { + value = "the_string" +} diff --git a/tests/apply/vars/main.tf b/tests/workflows/test-apply/deprecated_var/main.tf similarity index 100% rename from tests/apply/vars/main.tf rename to tests/workflows/test-apply/deprecated_var/main.tf diff --git a/tests/apply/error/main.tf b/tests/workflows/test-apply/error/main.tf similarity index 100% rename from tests/apply/error/main.tf rename to tests/workflows/test-apply/error/main.tf diff --git a/tests/apply/no_changes/main.tf b/tests/workflows/test-apply/no_changes/main.tf similarity index 100% rename from tests/apply/no_changes/main.tf rename to tests/workflows/test-apply/no_changes/main.tf diff --git a/tests/apply/no_plan/main.tf b/tests/workflows/test-apply/no_plan/main.tf similarity index 100% rename from tests/apply/no_plan/main.tf rename to tests/workflows/test-apply/no_plan/main.tf diff --git a/tests/apply/refresh_15/main.tf b/tests/workflows/test-apply/refresh_15/main.tf similarity index 100% rename from tests/apply/refresh_15/main.tf rename to tests/workflows/test-apply/refresh_15/main.tf diff --git a/tests/remote-state/test-bucket_12/main.tf b/tests/workflows/test-apply/remote/main.tf similarity index 100% rename from tests/remote-state/test-bucket_12/main.tf rename to tests/workflows/test-apply/remote/main.tf diff --git a/tests/apply/test.tfvars b/tests/workflows/test-apply/test.tfvars similarity index 100% rename from tests/apply/test.tfvars rename to tests/workflows/test-apply/test.tfvars diff --git a/tests/workflows/test-apply/vars/main.tf b/tests/workflows/test-apply/vars/main.tf new file mode 100644 index 00000000..2e928fee --- /dev/null +++ b/tests/workflows/test-apply/vars/main.tf @@ -0,0 +1,44 @@ +resource "random_string" "my_string" { + length = 11 +} + +output "output_string" { + value = "the_string" +} + +variable "my_var" { + type = string + default = "my_var_default" +} + +variable "my_var_from_file" { + type = string + default = "my_var_from_file_default" +} + +variable "complex_input" { + type = list(object({ + internal = number + external = number + protocol = string + })) + default = [ + { + internal = 8300 + external = 8300 + protocol = "tcp" + } + ] +} + +output "from_var" { + value = var.my_var +} + +output "from_varfile" { + value = var.my_var_from_file +} + +output "complex_output" { + value = join(",", [for input in var.complex_input : "${input.internal}:${input.external}:${input.protocol}"]) +} diff --git a/tests/plan/changes-only/main.tf b/tests/workflows/test-changes-only/main.tf similarity index 100% rename from tests/plan/changes-only/main.tf rename to tests/workflows/test-changes-only/main.tf diff --git a/tests/plan/plan/main.tf b/tests/workflows/test-check/changes/main.tf similarity index 100% rename from tests/plan/plan/main.tf rename to tests/workflows/test-check/changes/main.tf diff --git a/tests/plan/no_changes/main.tf b/tests/workflows/test-check/no_changes/main.tf similarity index 100% rename from tests/plan/no_changes/main.tf rename to tests/workflows/test-check/no_changes/main.tf diff --git a/tests/terraform-cloud/0.13/main.tf b/tests/workflows/test-cloud/0.13/main.tf similarity index 100% rename from tests/terraform-cloud/0.13/main.tf rename to tests/workflows/test-cloud/0.13/main.tf diff --git a/tests/terraform-cloud/0.13/my_variable.tfvars b/tests/workflows/test-cloud/0.13/my_variable.tfvars similarity index 100% rename from tests/terraform-cloud/0.13/my_variable.tfvars rename to tests/workflows/test-cloud/0.13/my_variable.tfvars diff --git a/tests/terraform-cloud/1.0/main.tf b/tests/workflows/test-cloud/1.0/main.tf similarity index 100% rename from tests/terraform-cloud/1.0/main.tf rename to tests/workflows/test-cloud/1.0/main.tf diff --git a/tests/terraform-cloud/1.0/my_variable.tfvars b/tests/workflows/test-cloud/1.0/my_variable.tfvars similarity index 100% rename from tests/terraform-cloud/1.0/my_variable.tfvars rename to tests/workflows/test-cloud/1.0/my_variable.tfvars diff --git a/tests/terraform-cloud/1.1/main.tf b/tests/workflows/test-cloud/1.1/main.tf similarity index 100% rename from tests/terraform-cloud/1.1/main.tf rename to tests/workflows/test-cloud/1.1/main.tf diff --git a/tests/terraform-cloud/1.1/my_variable.tfvars b/tests/workflows/test-cloud/1.1/my_variable.tfvars similarity index 100% rename from tests/terraform-cloud/1.1/my_variable.tfvars rename to tests/workflows/test-cloud/1.1/my_variable.tfvars diff --git a/tests/fmt/canonical/main.tf b/tests/workflows/test-fmt-check/canonical/main.tf similarity index 100% rename from tests/fmt/canonical/main.tf rename to tests/workflows/test-fmt-check/canonical/main.tf diff --git a/tests/fmt/canonical/subdir/main.tf b/tests/workflows/test-fmt-check/canonical/subdir/main.tf similarity index 100% rename from tests/fmt/canonical/subdir/main.tf rename to tests/workflows/test-fmt-check/canonical/subdir/main.tf diff --git a/tests/fmt/non-canonical/main.tf b/tests/workflows/test-fmt-check/non-canonical/main.tf similarity index 100% rename from tests/fmt/non-canonical/main.tf rename to tests/workflows/test-fmt-check/non-canonical/main.tf diff --git a/tests/fmt/non-canonical/subdir/main.tf b/tests/workflows/test-fmt-check/non-canonical/subdir/main.tf similarity index 100% rename from tests/fmt/non-canonical/subdir/main.tf rename to tests/workflows/test-fmt-check/non-canonical/subdir/main.tf diff --git a/tests/workflows/test-fmt/canonical/main.tf b/tests/workflows/test-fmt/canonical/main.tf new file mode 100644 index 00000000..5cc55884 --- /dev/null +++ b/tests/workflows/test-fmt/canonical/main.tf @@ -0,0 +1,4 @@ +resource "aws_s3_bucket" "hello" { + bucket = "asd" + bucket_prefix = "hgd" +} diff --git a/tests/workflows/test-fmt/canonical/subdir/main.tf b/tests/workflows/test-fmt/canonical/subdir/main.tf new file mode 100644 index 00000000..5cc55884 --- /dev/null +++ b/tests/workflows/test-fmt/canonical/subdir/main.tf @@ -0,0 +1,4 @@ +resource "aws_s3_bucket" "hello" { + bucket = "asd" + bucket_prefix = "hgd" +} diff --git a/tests/workflows/test-fmt/non-canonical/main.tf b/tests/workflows/test-fmt/non-canonical/main.tf new file mode 100644 index 00000000..46d6e863 --- /dev/null +++ b/tests/workflows/test-fmt/non-canonical/main.tf @@ -0,0 +1,10 @@ +resource "aws_s3_bucket" "hello" { + bucket = "asd" + bucket_prefix = "hgd" +} + +variable "test-var" { + type = string + description = "A test variable that is formatted wrong" + +} \ No newline at end of file diff --git a/tests/workflows/test-fmt/non-canonical/subdir/main.tf b/tests/workflows/test-fmt/non-canonical/subdir/main.tf new file mode 100644 index 00000000..bca928f7 --- /dev/null +++ b/tests/workflows/test-fmt/non-canonical/subdir/main.tf @@ -0,0 +1,4 @@ +resource "aws_s3_bucket" "hello" { + bucket = "asd" + bucket_prefix = "hgd" +} diff --git a/tests/http-module/main.tf b/tests/workflows/test-http/http-module/main.tf similarity index 100% rename from tests/http-module/main.tf rename to tests/workflows/test-http/http-module/main.tf diff --git a/tests/git-http-module/main.tf b/tests/workflows/test-http/main.tf similarity index 100% rename from tests/git-http-module/main.tf rename to tests/workflows/test-http/main.tf diff --git a/tests/new-workspace/main.tf b/tests/workflows/test-new-workspace/main.tf similarity index 100% rename from tests/new-workspace/main.tf rename to tests/workflows/test-new-workspace/main.tf diff --git a/tests/workflows/test-output/main.tf b/tests/workflows/test-output/main.tf new file mode 100644 index 00000000..0aa9bc90 --- /dev/null +++ b/tests/workflows/test-output/main.tf @@ -0,0 +1,116 @@ +terraform { + backend "s3" { + bucket = "terraform-github-actions" + key = "terraform-remote-state" + region = "eu-west-2" + } + required_version = "~> 0.12.0" +} + +output "my_number" { + value = 5 +} + +output "my_sensitive_number" { + value = 6 + sensitive = true +} + +output "my_string" { + value = "hello" +} + +output "my_sensitive_string" { + value = "password" + sensitive = true +} + +output "my_bool" { + value = true +} + +output "my_sensitive_bool" { + value = false + sensitive = true +} + +output "my_list" { + value = tolist(toset(["one", "two"])) +} + +output "my_sensitive_list" { + value = tolist(toset(["one", "two"])) + sensitive = true +} + +output "my_map" { + value = tomap({ + first = "one" + second = "two" + third = 3 + }) +} + +output "my_sensitive_map" { + value = tomap({ + first = "one" + second = "two" + third = 3 + }) + sensitive = true +} + +output "my_set" { + value = toset(["one", "two"]) +} + +output "my_sensitive_set" { + value = toset(["one", "two"]) + sensitive = true +} + +output "my_object" { + value = { + first = "one" + second = "two" + third = 3 + } +} + +output "my_sensitive_object" { + value = { + first = "one" + second = "two" + third = 3 + } + sensitive = true +} + +output "my_tuple" { + value = ["one", "two"] +} + +output "my_sensitive_tuple" { + value = ["one", "two"] + sensitive = true +} + +output "my_compound_output" { + value = { + first = tolist(toset(["one", "two"])) + second = toset(["one", "two"]) + third = 3 + } +} + +output "awkward_string" { + value = "hello \"there\", here are some 'quotes'." +} + +output "awkward_compound_output" { + value = { + nested = { + thevalue = ["hello \"there\", here are some 'quotes'."] + } + } +} diff --git a/tests/workflows/test-plan/changes-only/main.tf b/tests/workflows/test-plan/changes-only/main.tf new file mode 100644 index 00000000..e5192d4f --- /dev/null +++ b/tests/workflows/test-plan/changes-only/main.tf @@ -0,0 +1,12 @@ +variable "cause-changes" { + default = false +} + +variable "len" { + default = 5 +} + +resource "random_string" "the_string" { + count = var.cause-changes ? 1 : 0 + length = var.len +} diff --git a/tests/plan/error/main.tf b/tests/workflows/test-plan/error/main.tf similarity index 100% rename from tests/plan/error/main.tf rename to tests/workflows/test-plan/error/main.tf diff --git a/tests/workflows/test-plan/no_changes/main.tf b/tests/workflows/test-plan/no_changes/main.tf new file mode 100644 index 00000000..646825e0 --- /dev/null +++ b/tests/workflows/test-plan/no_changes/main.tf @@ -0,0 +1,3 @@ +locals { + hello = "world" +} \ No newline at end of file diff --git a/tests/workflows/test-plan/plan/main.tf b/tests/workflows/test-plan/plan/main.tf new file mode 100644 index 00000000..dee08246 --- /dev/null +++ b/tests/workflows/test-plan/plan/main.tf @@ -0,0 +1,7 @@ +resource "random_string" "my_string" { + length = 11 +} + +output "s" { + value = "string" +} diff --git a/tests/plan/plan_11/main.tf b/tests/workflows/test-plan/plan_11/main.tf similarity index 100% rename from tests/plan/plan_11/main.tf rename to tests/workflows/test-plan/plan_11/main.tf diff --git a/tests/plan/plan_12/main.tf b/tests/workflows/test-plan/plan_12/main.tf similarity index 100% rename from tests/plan/plan_12/main.tf rename to tests/workflows/test-plan/plan_12/main.tf diff --git a/tests/plan/plan_13/main.tf b/tests/workflows/test-plan/plan_13/main.tf similarity index 100% rename from tests/plan/plan_13/main.tf rename to tests/workflows/test-plan/plan_13/main.tf diff --git a/tests/plan/plan_14/main.tf b/tests/workflows/test-plan/plan_14/main.tf similarity index 100% rename from tests/plan/plan_14/main.tf rename to tests/workflows/test-plan/plan_14/main.tf diff --git a/tests/plan/plan_15/main.tf b/tests/workflows/test-plan/plan_15/main.tf similarity index 100% rename from tests/plan/plan_15/main.tf rename to tests/workflows/test-plan/plan_15/main.tf diff --git a/tests/plan/plan_15_4/main.tf b/tests/workflows/test-plan/plan_15_4/main.tf similarity index 100% rename from tests/plan/plan_15_4/main.tf rename to tests/workflows/test-plan/plan_15_4/main.tf diff --git a/tests/registry/main.tf b/tests/workflows/test-registry/main.tf similarity index 100% rename from tests/registry/main.tf rename to tests/workflows/test-registry/main.tf diff --git a/tests/registry/test-module/README.md b/tests/workflows/test-registry/test-module/README.md similarity index 100% rename from tests/registry/test-module/README.md rename to tests/workflows/test-registry/test-module/README.md diff --git a/tests/registry/test-module/main.tf b/tests/workflows/test-registry/test-module/main.tf similarity index 100% rename from tests/registry/test-module/main.tf rename to tests/workflows/test-registry/test-module/main.tf diff --git a/tests/ssh-module/main.tf b/tests/workflows/test-ssh/main.tf similarity index 100% rename from tests/ssh-module/main.tf rename to tests/workflows/test-ssh/main.tf diff --git a/tests/target/main.tf b/tests/workflows/test-target-replace/main.tf similarity index 100% rename from tests/target/main.tf rename to tests/workflows/test-target-replace/main.tf diff --git a/tests/validate/invalid/main.tf b/tests/workflows/test-validate/invalid/main.tf similarity index 100% rename from tests/validate/invalid/main.tf rename to tests/workflows/test-validate/invalid/main.tf diff --git a/tests/validate/report/error.json b/tests/workflows/test-validate/report/error.json similarity index 100% rename from tests/validate/report/error.json rename to tests/workflows/test-validate/report/error.json diff --git a/tests/validate/report/file_location.json b/tests/workflows/test-validate/report/file_location.json similarity index 100% rename from tests/validate/report/file_location.json rename to tests/workflows/test-validate/report/file_location.json diff --git a/tests/validate/report/line_num.json b/tests/workflows/test-validate/report/line_num.json similarity index 100% rename from tests/validate/report/line_num.json rename to tests/workflows/test-validate/report/line_num.json diff --git a/tests/validate/report/non_json.txt b/tests/workflows/test-validate/report/non_json.txt similarity index 100% rename from tests/validate/report/non_json.txt rename to tests/workflows/test-validate/report/non_json.txt diff --git a/tests/validate/report/test_convert_validate_report.sh b/tests/workflows/test-validate/report/test_convert_validate_report.sh similarity index 100% rename from tests/validate/report/test_convert_validate_report.sh rename to tests/workflows/test-validate/report/test_convert_validate_report.sh diff --git a/tests/validate/report/valid.json b/tests/workflows/test-validate/report/valid.json similarity index 100% rename from tests/validate/report/valid.json rename to tests/workflows/test-validate/report/valid.json diff --git a/tests/validate/valid/main.tf b/tests/workflows/test-validate/valid/main.tf similarity index 100% rename from tests/validate/valid/main.tf rename to tests/workflows/test-validate/valid/main.tf diff --git a/tests/validate/workspace_eval/main.tf b/tests/workflows/test-validate/workspace_eval/main.tf similarity index 100% rename from tests/validate/workspace_eval/main.tf rename to tests/workflows/test-validate/workspace_eval/main.tf diff --git a/tests/version/asdf/.tool-versions b/tests/workflows/test-version/asdf/.tool-versions similarity index 100% rename from tests/version/asdf/.tool-versions rename to tests/workflows/test-version/asdf/.tool-versions diff --git a/tests/version/cloud/main.tf b/tests/workflows/test-version/cloud/main.tf similarity index 100% rename from tests/version/cloud/main.tf rename to tests/workflows/test-version/cloud/main.tf diff --git a/tests/version/empty/README.md b/tests/workflows/test-version/empty/README.md similarity index 100% rename from tests/version/empty/README.md rename to tests/workflows/test-version/empty/README.md diff --git a/tests/version/local/main.tf b/tests/workflows/test-version/local/main.tf similarity index 100% rename from tests/version/local/main.tf rename to tests/workflows/test-version/local/main.tf diff --git a/tests/version/local/terraform.tfstate b/tests/workflows/test-version/local/terraform.tfstate similarity index 100% rename from tests/version/local/terraform.tfstate rename to tests/workflows/test-version/local/terraform.tfstate diff --git a/tests/version/providers/0.11/main.tf b/tests/workflows/test-version/providers/0.11/main.tf similarity index 100% rename from tests/version/providers/0.11/main.tf rename to tests/workflows/test-version/providers/0.11/main.tf diff --git a/tests/version/providers/0.12/main.tf b/tests/workflows/test-version/providers/0.12/main.tf similarity index 100% rename from tests/version/providers/0.12/main.tf rename to tests/workflows/test-version/providers/0.12/main.tf diff --git a/tests/version/providers/0.13/versions.tf b/tests/workflows/test-version/providers/0.13/versions.tf similarity index 100% rename from tests/version/providers/0.13/versions.tf rename to tests/workflows/test-version/providers/0.13/versions.tf diff --git a/tests/version/range/main.tf b/tests/workflows/test-version/range/main.tf similarity index 100% rename from tests/version/range/main.tf rename to tests/workflows/test-version/range/main.tf diff --git a/tests/version/required_version/main.tf b/tests/workflows/test-version/required_version/main.tf similarity index 100% rename from tests/version/required_version/main.tf rename to tests/workflows/test-version/required_version/main.tf diff --git a/tests/version/state/main.tf b/tests/workflows/test-version/state/main.tf similarity index 100% rename from tests/version/state/main.tf rename to tests/workflows/test-version/state/main.tf diff --git a/tests/version/terraform-cloud/main.tf b/tests/workflows/test-version/terraform-cloud/main.tf similarity index 100% rename from tests/version/terraform-cloud/main.tf rename to tests/workflows/test-version/terraform-cloud/main.tf diff --git a/tests/version/tfenv/.terraform-version b/tests/workflows/test-version/tfenv/.terraform-version similarity index 100% rename from tests/version/tfenv/.terraform-version rename to tests/workflows/test-version/tfenv/.terraform-version diff --git a/tests/version/tfenv/main.tf b/tests/workflows/test-version/tfenv/main.tf similarity index 100% rename from tests/version/tfenv/main.tf rename to tests/workflows/test-version/tfenv/main.tf diff --git a/tests/version/tfswitch/.tfswitchrc b/tests/workflows/test-version/tfswitch/.tfswitchrc similarity index 100% rename from tests/version/tfswitch/.tfswitchrc rename to tests/workflows/test-version/tfswitch/.tfswitchrc diff --git a/tests/version/tfswitch/main.tf b/tests/workflows/test-version/tfswitch/main.tf similarity index 100% rename from tests/version/tfswitch/main.tf rename to tests/workflows/test-version/tfswitch/main.tf diff --git a/tests/workflows/test-workflow-commands/main.tf b/tests/workflows/test-workflow-commands/main.tf new file mode 100644 index 00000000..dee08246 --- /dev/null +++ b/tests/workflows/test-workflow-commands/main.tf @@ -0,0 +1,7 @@ +resource "random_string" "my_string" { + length = 11 +} + +output "s" { + value = "string" +} From b50436b035b9981c7f321dea09cacec40b3b6a41 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sun, 1 May 2022 20:04:42 +0100 Subject: [PATCH 11/18] Fix header value bugs --- image/src/github_pr_comment/__main__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/image/src/github_pr_comment/__main__.py b/image/src/github_pr_comment/__main__.py index 69bc5e69..cddd02c4 100644 --- a/image/src/github_pr_comment/__main__.py +++ b/image/src/github_pr_comment/__main__.py @@ -203,14 +203,14 @@ def get_comment(action_inputs: PlanPrInputs, backend_fingerprint: bytes) -> Terr plan_modifier = {} if target := os.environ.get('INPUT_TARGET'): - plan_modifier['target'] = sorted(t.strip() for t in target.replace(',', '\n', ).split('\n')) + plan_modifier['target'] = sorted(t.strip() for t in target.replace(',', '\n', ).split('\n') if t.strip()) if replace := os.environ.get('INPUT_REPLACE'): - plan_modifier['replace'] = sorted(t.strip() for t in replace.replace(',', '\n', ).split('\n')) + plan_modifier['replace'] = sorted(t.strip() for t in replace.replace(',', '\n', ).split('\n') if t.strip()) if plan_modifier: debug(f'Plan modifier: {plan_modifier}') - headers['plan_modifier'] = hashlib.sha256(canonicaljson.encode_canonical_json(plan_modifier)) + headers['plan_modifier'] = hashlib.sha256(canonicaljson.encode_canonical_json(plan_modifier)).hexdigest() return find_comment(github, issue_url, username, headers, legacy_description) From 4e1ee355e11c585c4d55d5a1d068914562e3009a Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sun, 1 May 2022 20:27:51 +0100 Subject: [PATCH 12/18] Don't select prerelease versions when using latest terraform --- image/src/terraform/versions.py | 14 +++++++++++++- image/src/terraform_version/__main__.py | 9 ++++----- image/src/terraform_version/asdf.py | 4 ++-- image/src/terraform_version/env.py | 5 ++--- image/src/terraform_version/remote_state.py | 6 +++--- .../src/terraform_version/remote_workspace.py | 4 ++-- .../src/terraform_version/required_version.py | 4 ++-- image/src/terraform_version/tfenv.py | 4 ++-- tests/terraform_version/test_latest.py | 19 +++++++++++++++++++ 9 files changed, 49 insertions(+), 20 deletions(-) create mode 100644 tests/terraform_version/test_latest.py diff --git a/image/src/terraform/versions.py b/image/src/terraform/versions.py index 2807eee3..8d3ac7ee 100644 --- a/image/src/terraform/versions.py +++ b/image/src/terraform/versions.py @@ -4,7 +4,7 @@ import re from functools import total_ordering -from typing import Any, cast, Iterable, Literal +from typing import Any, cast, Iterable, Literal, Optional import requests @@ -183,12 +183,24 @@ def compare() -> int: # ~> x.x.x return version.major == self.major and version.minor == self.minor and version.patch >= self.patch +def latest_non_prerelease_version(versions: Iterable[Version]) -> Optional[Version]: + """Return the latest non prerelease version of the given versions.""" + + for v in sorted(versions, reverse=True): + if not v.pre_release: + return v def latest_version(versions: Iterable[Version]) -> Version: """Return the latest version of the given versions.""" return sorted(versions, reverse=True)[0] +def earliest_non_prerelease_version(versions: Iterable[Version]) -> Optional[Version]: + """Return the earliest non prerelease version of the given versions.""" + + for v in sorted(versions): + if not v.pre_release: + return v def earliest_version(versions: Iterable[Version]) -> Version: """Return the earliest version of the given versions.""" diff --git a/image/src/terraform_version/__main__.py b/image/src/terraform_version/__main__.py index 8602e6e2..77f2e86d 100644 --- a/image/src/terraform_version/__main__.py +++ b/image/src/terraform_version/__main__.py @@ -8,17 +8,16 @@ from pathlib import Path from typing import Optional, cast -from terraform_version.remote_state import get_backend_constraints, read_backend_config_vars, try_guess_state_version - from github_actions.debug import debug from github_actions.env import ActionsEnv, GithubEnv from github_actions.inputs import InitInputs from terraform.download import get_executable from terraform.module import load_module, get_backend_type -from terraform.versions import apply_constraints, get_terraform_versions, latest_version, Version, Constraint +from terraform.versions import apply_constraints, get_terraform_versions, Version, Constraint, latest_non_prerelease_version from terraform_version.asdf import try_read_asdf from terraform_version.env import try_read_env from terraform_version.local_state import try_read_local_state +from terraform_version.remote_state import get_backend_constraints, read_backend_config_vars, try_guess_state_version from terraform_version.remote_workspace import try_get_remote_workspace_version from terraform_version.required_version import try_get_required_version from terraform_version.tfenv import try_read_tfenv @@ -69,7 +68,7 @@ def determine_version(inputs: InitInputs, cli_config_path: Path, actions_env: Ac except Exception as e: debug('Failed to get backend config') debug(str(e)) - return latest_version(versions) + return latest_non_prerelease_version(versions) if backend_type not in ['remote', 'local']: if version := try_guess_state_version(inputs, module, versions): @@ -82,7 +81,7 @@ def determine_version(inputs: InitInputs, cli_config_path: Path, actions_env: Ac return version sys.stdout.write('Terraform version not specified, using the latest version\n') - return latest_version(versions) + return latest_non_prerelease_version(versions) def switch(version: Version) -> None: diff --git a/image/src/terraform_version/asdf.py b/image/src/terraform_version/asdf.py index 748c1060..558428f7 100644 --- a/image/src/terraform_version/asdf.py +++ b/image/src/terraform_version/asdf.py @@ -8,7 +8,7 @@ from github_actions.debug import debug from github_actions.inputs import InitInputs -from terraform.versions import latest_version, Version +from terraform.versions import Version, latest_non_prerelease_version def parse_asdf(tool_versions: str, versions: Iterable[Version]) -> Version: @@ -17,7 +17,7 @@ def parse_asdf(tool_versions: str, versions: Iterable[Version]) -> Version: for line in tool_versions.splitlines(): if match := re.match(r'^\s*terraform\s+([^\s#]+)', line.strip()): if match.group(1) == 'latest': - return latest_version(v for v in versions if not v.pre_release) + return latest_non_prerelease_version(v for v in versions if not v.pre_release) return Version(match.group(1)) raise Exception('No version for terraform found in .tool-versions') diff --git a/image/src/terraform_version/env.py b/image/src/terraform_version/env.py index 2c99abb4..3efb2171 100644 --- a/image/src/terraform_version/env.py +++ b/image/src/terraform_version/env.py @@ -1,11 +1,10 @@ from __future__ import annotations -import sys from typing import Iterable, Optional from github_actions.debug import debug from github_actions.env import ActionsEnv -from terraform.versions import Version, Constraint, apply_constraints, latest_version +from terraform.versions import Version, Constraint, apply_constraints, latest_non_prerelease_version def try_read_env(actions_env: ActionsEnv, versions: Iterable[Version]) -> Optional[Version]: @@ -18,7 +17,7 @@ def try_read_env(actions_env: ActionsEnv, versions: Iterable[Version]) -> Option valid_versions = list(apply_constraints(versions, [Constraint(c) for c in constraint.split(',')])) if not valid_versions: return None - return latest_version(valid_versions) + return latest_non_prerelease_version(valid_versions) except Exception as exception: debug(str(exception)) diff --git a/image/src/terraform_version/remote_state.py b/image/src/terraform_version/remote_state.py index 3e51aef8..1cde9886 100644 --- a/image/src/terraform_version/remote_state.py +++ b/image/src/terraform_version/remote_state.py @@ -16,7 +16,7 @@ from terraform.download import get_executable from terraform.exec import init_args from terraform.module import load_backend_config_file, TerraformModule -from terraform.versions import apply_constraints, Constraint, Version, earliest_version +from terraform.versions import apply_constraints, Constraint, Version, earliest_version, earliest_non_prerelease_version def read_backend_config_vars(init_inputs: InitInputs) -> dict[str, str]: @@ -205,7 +205,7 @@ def guess_state_version(inputs: InitInputs, module: TerraformModule, versions: I candidate_versions = list(versions) while candidate_versions: - result = try_init(earliest_version(candidate_versions), args, inputs.get('INPUT_WORKSPACE', 'default'), backend_tf) + result = try_init(earliest_non_prerelease_version(candidate_versions), args, inputs.get('INPUT_WORKSPACE', 'default'), backend_tf) if isinstance(result, Version): return result elif isinstance(result, Constraint): @@ -213,7 +213,7 @@ def guess_state_version(inputs: InitInputs, module: TerraformModule, versions: I elif result is None: return None else: - candidate_versions = list(apply_constraints(candidate_versions, [Constraint(f'!={earliest_version}')])) + candidate_versions = list(apply_constraints(candidate_versions, [Constraint(f'!={earliest_version(candidate_versions)}')])) return None diff --git a/image/src/terraform_version/remote_workspace.py b/image/src/terraform_version/remote_workspace.py index 3007ebcb..e772ad8a 100644 --- a/image/src/terraform_version/remote_workspace.py +++ b/image/src/terraform_version/remote_workspace.py @@ -5,7 +5,7 @@ from github_actions.inputs import InitInputs from terraform.cloud import get_workspace from terraform.module import TerraformModule, get_remote_backend_config, get_cloud_config -from terraform.versions import Version, latest_version +from terraform.versions import Version, latest_non_prerelease_version def get_remote_workspace_version(inputs: InitInputs, module: TerraformModule, cli_config_path: Path, versions: Iterable[Version]) -> Optional[Version]: @@ -30,7 +30,7 @@ def get_remote_workspace_version(inputs: InitInputs, module: TerraformModule, cl if workspace_info := get_workspace(backend_config, inputs['INPUT_WORKSPACE']): version = str(workspace_info['attributes']['terraform-version']) if version == 'latest': - return latest_version(versions) + return latest_non_prerelease_version(versions) else: return Version(version) diff --git a/image/src/terraform_version/required_version.py b/image/src/terraform_version/required_version.py index cf4aa6a1..6aecba4e 100644 --- a/image/src/terraform_version/required_version.py +++ b/image/src/terraform_version/required_version.py @@ -2,7 +2,7 @@ from github_actions.debug import debug from terraform.module import get_version_constraints, TerraformModule -from terraform.versions import Version, apply_constraints, latest_version +from terraform.versions import Version, apply_constraints, latest_non_prerelease_version def get_required_version(module: TerraformModule, versions: Iterable[Version]) -> Optional[Version]: @@ -14,7 +14,7 @@ def get_required_version(module: TerraformModule, versions: Iterable[Version]) - if not valid_versions: raise RuntimeError(f'No versions of terraform match the required_version constraints {constraints}\n') - return latest_version(valid_versions) + return latest_non_prerelease_version(valid_versions) def try_get_required_version(module: TerraformModule, versions: Iterable[Version]) -> Optional[Version]: diff --git a/image/src/terraform_version/tfenv.py b/image/src/terraform_version/tfenv.py index 1dd20ba0..3842faeb 100644 --- a/image/src/terraform_version/tfenv.py +++ b/image/src/terraform_version/tfenv.py @@ -8,7 +8,7 @@ from github_actions.debug import debug from github_actions.inputs import InitInputs -from terraform.versions import latest_version, Version +from terraform.versions import latest_version, Version, latest_non_prerelease_version def parse_tfenv(terraform_version_file: str, versions: Iterable[Version]) -> Version: @@ -23,7 +23,7 @@ def parse_tfenv(terraform_version_file: str, versions: Iterable[Version]) -> Ver version = terraform_version_file.strip() if version == 'latest': - return latest_version(v for v in versions if not v.pre_release) + return latest_non_prerelease_version(v for v in versions if not v.pre_release) if version.startswith('latest:'): version_regex = version.split(':', maxsplit=1)[1] diff --git a/tests/terraform_version/test_latest.py b/tests/terraform_version/test_latest.py new file mode 100644 index 00000000..6028e88c --- /dev/null +++ b/tests/terraform_version/test_latest.py @@ -0,0 +1,19 @@ +from __future__ import annotations +from terraform.versions import Version, earliest_version, latest_version, earliest_non_prerelease_version, latest_non_prerelease_version + + +def test_latest(): + versions = [ + Version('0.13.6-alpha-23'), + Version('0.13.6'), + Version('1.1.8'), + Version('1.1.9'), + Version('1.1.7'), + Version('1.1.0-alpha20210811'), + Version('1.2.0-alpha20225555') + ] + + assert earliest_version(versions) == Version('0.13.6-alpha-23') + assert latest_version(versions) == Version('1.2.0-alpha20225555') + assert earliest_non_prerelease_version(versions) == Version('0.13.6') + assert latest_non_prerelease_version(versions) == Version('1.1.9') From c2d303710bf35ac45bed3ba4feac1b5b5b687aa5 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sun, 1 May 2022 20:45:32 +0100 Subject: [PATCH 13/18] Fix paths --- .github/workflows/test-changes-only.yaml | 20 +++++------ .github/workflows/test-cloud.yaml | 38 ++++++++++---------- .github/workflows/test-output.yaml | 2 +- .github/workflows/test-plan.yaml | 4 +-- tests/workflows/test-plan/test.tfvars | 2 ++ tests/workflows/test-plan/vars/main.tf | 44 ++++++++++++++++++++++++ 6 files changed, 78 insertions(+), 32 deletions(-) create mode 100644 tests/workflows/test-plan/test.tfvars create mode 100644 tests/workflows/test-plan/vars/main.tf diff --git a/.github/workflows/test-changes-only.yaml b/.github/workflows/test-changes-only.yaml index 151d7a4e..262cd318 100644 --- a/.github/workflows/test-changes-only.yaml +++ b/.github/workflows/test-changes-only.yaml @@ -17,8 +17,8 @@ jobs: uses: ./terraform-plan id: plan with: - label: no_changes - path: tests/plan/changes-only + label: test-changes-only change-only THIS SHOULD NOT BE A COMMENT + path: tests/workflows/test-changes-only add_github_comment: changes-only - name: Verify outputs @@ -34,7 +34,7 @@ jobs: uses: ./terraform-apply id: apply with: - label: no_changes + label: test-changes-only change-only THIS SHOULD NOT BE A COMMENT path: tests/workflows/test-changes-only - name: Check failure-reason @@ -57,7 +57,7 @@ jobs: uses: ./terraform-plan id: changes-plan with: - label: change_then_no_changes + label: test-changes-only change_then_no_changes path: tests/workflows/test-changes-only variables: | cause-changes=true @@ -76,7 +76,7 @@ jobs: uses: ./terraform-plan id: plan with: - label: change_then_no_changes + label: test-changes-only change_then_no_changes path: tests/workflows/test-changes-only variables: | cause-changes=false @@ -95,7 +95,7 @@ jobs: uses: ./terraform-apply id: apply with: - label: change_then_no_changes + label: test-changes-only change_then_no_changes path: tests/workflows/test-changes-only variables: | cause-changes=false @@ -121,7 +121,7 @@ jobs: id: plan with: path: tests/workflows/test-changes-only - label: no_changes_then_changes + label: test-changes-only no_changes_then_changes variables: | cause-changes=false add_github_comment: changes-only @@ -141,7 +141,7 @@ jobs: continue-on-error: true with: path: tests/workflows/test-changes-only - label: no_changes_then_changes + label: test-changes-only no_changes_then_changes variables: | cause-changes=true @@ -170,7 +170,7 @@ jobs: uses: ./terraform-plan with: path: tests/workflows/test-changes-only - label: apply_when_plan_has_changed + label: test-changes-only apply_when_plan_has_changed variables: | cause-changes=true @@ -180,7 +180,7 @@ jobs: continue-on-error: true with: path: tests/workflows/test-changes-only - label: apply_when_plan_has_changed + label: test-changes-only apply_when_plan_has_changed variables: | cause-changes=true len=4 diff --git a/.github/workflows/test-cloud.yaml b/.github/workflows/test-cloud.yaml index 6427cbf3..5caca69d 100644 --- a/.github/workflows/test-cloud.yaml +++ b/.github/workflows/test-cloud.yaml @@ -18,21 +18,21 @@ jobs: - name: Create a new workspace with no existing workspaces uses: ./terraform-new-workspace with: - path: tests/workflows/terraform-cloud/${{ matrix.tf_version }} + path: tests/workflows/test-cloud/${{ matrix.tf_version }} workspace: ${{ github.head_ref }}-1 backend_config: token=${{ secrets.TF_API_TOKEN }} - name: Create a new workspace when it doesn't exist uses: ./terraform-new-workspace with: - path: tests/workflows/terraform-cloud/${{ matrix.tf_version }} + path: tests/workflows/test-cloud/${{ matrix.tf_version }} workspace: ${{ github.head_ref }}-2 backend_config: token=${{ secrets.TF_API_TOKEN }} - name: Create a new workspace when it already exists uses: ./terraform-new-workspace with: - path: tests/workflows/terraform-cloud/${{ matrix.tf_version }} + path: tests/workflows/test-cloud/${{ matrix.tf_version }} workspace: ${{ github.head_ref }}-2 backend_config: token=${{ secrets.TF_API_TOKEN }} @@ -40,12 +40,12 @@ jobs: uses: ./terraform-apply id: auto_apply with: - path: tests/workflows/terraform-cloud/${{ matrix.tf_version }} + path: tests/workflows/test-cloud/${{ matrix.tf_version }} workspace: ${{ github.head_ref }}-1 backend_config: token=${{ secrets.TF_API_TOKEN }} auto_approve: true var_file: | - tests/workflows/terraform-cloud/${{ matrix.tf_version }}/my_variable.tfvars + tests/workflows/test-cloud/${{ matrix.tf_version }}/my_variable.tfvars variables: | from_variables="from_variables" @@ -80,7 +80,7 @@ jobs: uses: ./terraform-output id: output with: - path: tests/workflows/terraform-cloud/${{ matrix.tf_version }} + path: tests/workflows/test-cloud/${{ matrix.tf_version }} workspace: ${{ github.head_ref }}-1 backend_config: token=${{ secrets.TF_API_TOKEN }} @@ -95,9 +95,9 @@ jobs: run: | mkdir fixed-workspace-name if [[ "${{ matrix.tf_version }}" == "0.13" ]]; then - sed -e 's/prefix.*/name = "github-actions-0-13-${{ github.head_ref }}-1"/' tests/workflows/terraform-cloud/${{ matrix.tf_version }}/main.tf > fixed-workspace-name/main.tf + sed -e 's/prefix.*/name = "github-actions-0-13-${{ github.head_ref }}-1"/' tests/workflows/test-cloud/${{ matrix.tf_version }}/main.tf > fixed-workspace-name/main.tf else - sed -e 's/prefix.*/name = "github-actions-1-1-${{ github.head_ref }}-1"/' tests/workflows/terraform-cloud/${{ matrix.tf_version }}/main.tf > fixed-workspace-name/main.tf + sed -e 's/prefix.*/name = "github-actions-1-1-${{ github.head_ref }}-1"/' tests/workflows/test-cloud/${{ matrix.tf_version }}/main.tf > fixed-workspace-name/main.tf fi - name: Get outputs @@ -117,11 +117,11 @@ jobs: - name: Check no changes uses: ./terraform-check with: - path: tests/workflows/terraform-cloud/${{ matrix.tf_version }} + path: tests/workflows/test-cloud/${{ matrix.tf_version }} workspace: ${{ github.head_ref }}-1 backend_config: token=${{ secrets.TF_API_TOKEN }} var_file: | - tests/workflows/terraform-cloud/${{ matrix.tf_version }}/my_variable.tfvars + tests/workflows/test-cloud/${{ matrix.tf_version }}/my_variable.tfvars variables: | from_variables="from_variables" @@ -130,11 +130,11 @@ jobs: id: check continue-on-error: true with: - path: tests/workflows/terraform-cloud/${{ matrix.tf_version }} + path: tests/workflows/test-cloud/${{ matrix.tf_version }} workspace: ${{ github.head_ref }}-1 backend_config: token=${{ secrets.TF_API_TOKEN }} var_file: | - tests/workflows/terraform-cloud/${{ matrix.tf_version }}/my_variable.tfvars + tests/workflows/test-cloud/${{ matrix.tf_version }}/my_variable.tfvars variables: | from_variables="Changed!" @@ -153,7 +153,7 @@ jobs: - name: Destroy workspace uses: ./terraform-destroy-workspace with: - path: tests/workflows/terraform-cloud/${{ matrix.tf_version }} + path: tests/workflows/test-cloud/${{ matrix.tf_version }} workspace: ${{ github.head_ref }}-1 backend_config: token=${{ secrets.TF_API_TOKEN }} @@ -163,11 +163,11 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - path: tests/workflows/terraform-cloud/${{ matrix.tf_version }} + path: tests/workflows/test-cloud/${{ matrix.tf_version }} workspace: ${{ github.head_ref }}-2 backend_config: token=${{ secrets.TF_API_TOKEN }} var_file: | - tests/workflows/terraform-cloud/${{ matrix.tf_version }}/my_variable.tfvars + tests/workflows/test-cloud/${{ matrix.tf_version }}/my_variable.tfvars variables: | from_variables="from_variables" @@ -189,11 +189,11 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - path: tests/workflows/terraform-cloud/${{ matrix.tf_version }} + path: tests/workflows/test-cloud/${{ matrix.tf_version }} workspace: ${{ github.head_ref }}-2 backend_config: token=${{ secrets.TF_API_TOKEN }} var_file: | - tests/workflows/terraform-cloud/${{ matrix.tf_version }}/my_variable.tfvars + tests/workflows/test-cloud/${{ matrix.tf_version }}/my_variable.tfvars variables: | from_variables="from_variables" @@ -227,7 +227,7 @@ jobs: - name: Destroy the last workspace uses: ./terraform-destroy-workspace with: - path: tests/workflows/terraform-cloud/${{ matrix.tf_version }} + path: tests/workflows/test-cloud/${{ matrix.tf_version }} workspace: ${{ github.head_ref }}-2 backend_config: token=${{ secrets.TF_API_TOKEN }} @@ -236,7 +236,7 @@ jobs: continue-on-error: true id: destroy-non-existant-workspace with: - path: tests/workflows/terraform-cloud/${{ matrix.tf_version }} + path: tests/workflows/test-cloud/${{ matrix.tf_version }} workspace: ${{ github.head_ref }}-1 backend_config: token=${{ secrets.TF_API_TOKEN }} - name: Check failed to destroy diff --git a/.github/workflows/test-output.yaml b/.github/workflows/test-output.yaml index 8ddcba5b..d48775bc 100644 --- a/.github/workflows/test-output.yaml +++ b/.github/workflows/test-output.yaml @@ -19,7 +19,7 @@ jobs: uses: ./terraform-output id: terraform-output with: - path: tests/workflows/test-output/test-bucket_12 + path: tests/workflows/test-output - name: Print the outputs run: | diff --git a/.github/workflows/test-plan.yaml b/.github/workflows/test-plan.yaml index 77984075..9fecc22e 100644 --- a/.github/workflows/test-plan.yaml +++ b/.github/workflows/test-plan.yaml @@ -453,10 +453,10 @@ jobs: - name: Plan uses: ./terraform-plan with: - path: tests/apply/vars + path: tests/workflows/test-plan/vars variables: | my_var="single" - var_file: tests/apply/test.tfvars + var_file: tests/workflows/test-plan/test.tfvars plan_change_run_commands: runs-on: ubuntu-latest diff --git a/tests/workflows/test-plan/test.tfvars b/tests/workflows/test-plan/test.tfvars new file mode 100644 index 00000000..368d66db --- /dev/null +++ b/tests/workflows/test-plan/test.tfvars @@ -0,0 +1,2 @@ +my_var_from_file="monkey" +my_var="this should be overridden" diff --git a/tests/workflows/test-plan/vars/main.tf b/tests/workflows/test-plan/vars/main.tf new file mode 100644 index 00000000..2e928fee --- /dev/null +++ b/tests/workflows/test-plan/vars/main.tf @@ -0,0 +1,44 @@ +resource "random_string" "my_string" { + length = 11 +} + +output "output_string" { + value = "the_string" +} + +variable "my_var" { + type = string + default = "my_var_default" +} + +variable "my_var_from_file" { + type = string + default = "my_var_from_file_default" +} + +variable "complex_input" { + type = list(object({ + internal = number + external = number + protocol = string + })) + default = [ + { + internal = 8300 + external = 8300 + protocol = "tcp" + } + ] +} + +output "from_var" { + value = var.my_var +} + +output "from_varfile" { + value = var.my_var_from_file +} + +output "complex_output" { + value = join(",", [for input in var.complex_input : "${input.internal}:${input.external}:${input.protocol}"]) +} From fda43c428d6a4d0aa572604848975763dfcde4f0 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sun, 1 May 2022 22:22:36 +0100 Subject: [PATCH 14/18] Use job name in label --- .github/workflows/test-apply.yaml | 8 ++++---- .github/workflows/test-plan.yaml | 2 +- .github/workflows/test-registry.yaml | 8 ++++---- .github/workflows/test-ssh.yaml | 4 ++-- .github/workflows/test-target-replace.yaml | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test-apply.yaml b/.github/workflows/test-apply.yaml index 7d17eda8..046c557b 100644 --- a/.github/workflows/test-apply.yaml +++ b/.github/workflows/test-apply.yaml @@ -470,7 +470,7 @@ jobs: uses: ./terraform-plan with: path: tests/workflows/test-apply/vars - label: TestLabel + label: test-apply apply_label variables: my_var="world" var_file: tests/workflows/test-apply/test.tfvars @@ -479,7 +479,7 @@ jobs: id: output with: path: tests/workflows/test-apply/vars - label: TestLabel + label: test-apply apply_label variables: my_var="world" var_file: tests/workflows/test-apply/test.tfvars @@ -580,14 +580,14 @@ jobs: - name: Plan uses: ./terraform-plan with: - label: User PAT + label: test-apply apply_user_token path: tests/workflows/test-apply/changes - name: Apply uses: ./terraform-apply id: output with: - label: User PAT + label: test-apply apply_user_token path: tests/workflows/test-apply/changes - name: Verify outputs diff --git a/.github/workflows/test-plan.yaml b/.github/workflows/test-plan.yaml index 9fecc22e..d091503a 100644 --- a/.github/workflows/test-plan.yaml +++ b/.github/workflows/test-plan.yaml @@ -492,4 +492,4 @@ jobs: - name: Plan uses: ./terraform-plan with: - label: Optional path + label: test-plan default_path diff --git a/.github/workflows/test-registry.yaml b/.github/workflows/test-registry.yaml index 85d540fb..ce8b6bec 100644 --- a/.github/workflows/test-registry.yaml +++ b/.github/workflows/test-registry.yaml @@ -20,14 +20,14 @@ jobs: TERRAFORM_CLOUD_TOKENS: app.terraform.io=${{ secrets.TF_API_TOKEN }} with: path: tests/workflows/test-registry - label: Single registry + label: test-registry registry_module - name: Apply uses: ./terraform-apply id: output with: path: tests/workflows/test-registry - label: Single registry + label: test-registry registry_module env: TERRAFORM_CLOUD_TOKENS: app.terraform.io=${{ secrets.TF_API_TOKEN }} @@ -55,14 +55,14 @@ jobs: uses: ./terraform-plan with: path: tests/workflows/test-registry - label: Multiple registries + label: test-registry multiple_registry_module - name: Apply uses: ./terraform-apply id: output with: path: tests/workflows/test-registry - label: Multiple registries + label: test-registry multiple_registry_module - name: Verify outputs run: | diff --git a/.github/workflows/test-ssh.yaml b/.github/workflows/test-ssh.yaml index 56fa49d7..55870e80 100644 --- a/.github/workflows/test-ssh.yaml +++ b/.github/workflows/test-ssh.yaml @@ -29,7 +29,7 @@ jobs: TERRAFORM_SSH_KEY: ${{ secrets.TERRAFORM_SSH_KEY }} with: path: tests/workflows/test-ssh - label: SSH Module + label: test-ssh ssh_key - name: Verify outputs run: | @@ -51,7 +51,7 @@ jobs: id: plan with: path: tests/workflows/test-ssh - label: SSH Module + label: test-ssh no_ssh_key add_github_comment: false - name: Check failed diff --git a/.github/workflows/test-target-replace.yaml b/.github/workflows/test-target-replace.yaml index f4162e45..395f5225 100644 --- a/.github/workflows/test-target-replace.yaml +++ b/.github/workflows/test-target-replace.yaml @@ -18,7 +18,7 @@ jobs: uses: ./terraform-plan id: plan with: - label: No targeted changes + label: test-target-replace plan_targeting path: tests/workflows/test-target-replace target: | random_string.notpresent @@ -248,7 +248,7 @@ jobs: uses: ./terraform-plan id: plan with: - label: No targeted changes + label: test-target-replace remote_plan_targeting path: tests/workflows/test-target-replace target: | random_string.notpresent From 037fa185bd1cc0fdddbc61cd764ad6d6a06e43df Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sun, 1 May 2022 22:52:30 +0100 Subject: [PATCH 15/18] Don't match comments with a label if no label --- .github/workflows/test-ssh.yaml | 2 +- image/src/github_pr_comment/__main__.py | 3 +-- image/src/github_pr_comment/comment.py | 6 +++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-ssh.yaml b/.github/workflows/test-ssh.yaml index 55870e80..be19cbc9 100644 --- a/.github/workflows/test-ssh.yaml +++ b/.github/workflows/test-ssh.yaml @@ -20,7 +20,7 @@ jobs: TERRAFORM_SSH_KEY: ${{ secrets.TERRAFORM_SSH_KEY }} with: path: tests/workflows/test-ssh - label: SSH Module + label: test-ssh ssh_key - name: Apply uses: ./terraform-apply diff --git a/image/src/github_pr_comment/__main__.py b/image/src/github_pr_comment/__main__.py index cddd02c4..73c54683 100644 --- a/image/src/github_pr_comment/__main__.py +++ b/image/src/github_pr_comment/__main__.py @@ -198,8 +198,7 @@ def get_comment(action_inputs: PlanPrInputs, backend_fingerprint: bytes) -> Terr if backend_type := os.environ.get('TERRAFORM_BACKEND_TYPE'): headers['backend_type'] = backend_type - if label := os.environ.get('INPUT_LABEL'): - headers['label'] = label + headers['label'] = os.environ.get('INPUT_LABEL') or None plan_modifier = {} if target := os.environ.get('INPUT_TARGET'): diff --git a/image/src/github_pr_comment/comment.py b/image/src/github_pr_comment/comment.py index 280e5293..1367c693 100644 --- a/image/src/github_pr_comment/comment.py +++ b/image/src/github_pr_comment/comment.py @@ -194,10 +194,14 @@ def matching_headers(comment: TerraformComment, headers: dict[str, str]) -> bool Does a comment have all the specified headers Additional headers may be present in the comment, they are ignored if not specified in the headers argument. + If a header should NOT be present in the comment, specify a header with a value of None """ for header, value in headers.items(): - if header not in comment.headers: + if value is not None and header not in comment.headers: + return False + + if value is None and header in comment.headers: return False if comment.headers[header] != value: From a32bf192ddb082e179edbc137c8e47e8c4f59be2 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Mon, 2 May 2022 09:00:01 +0100 Subject: [PATCH 16/18] Don't match comments with a label if no label --- image/src/github_pr_comment/comment.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/image/src/github_pr_comment/comment.py b/image/src/github_pr_comment/comment.py index 1367c693..61f788e0 100644 --- a/image/src/github_pr_comment/comment.py +++ b/image/src/github_pr_comment/comment.py @@ -198,13 +198,10 @@ def matching_headers(comment: TerraformComment, headers: dict[str, str]) -> bool """ for header, value in headers.items(): - if value is not None and header not in comment.headers: - return False - if value is None and header in comment.headers: return False - if comment.headers[header] != value: + if value is not None and comment.headers.get(header) != value: return False return True @@ -263,7 +260,7 @@ def find_comment(github: GithubApi, issue_url: IssueUrl, username: str, headers: return TerraformComment( issue_url=issue_url, comment_url=None, - headers=headers, + headers={k: v for k, v in headers.items() if v is not None}, description='', summary='', body='', From 68fa447af9aa13008ca08addfdb1b3a129bc7d95 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Mon, 2 May 2022 10:04:58 +0100 Subject: [PATCH 17/18] Insert known headers into legacy comment --- image/src/github_pr_comment/comment.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/image/src/github_pr_comment/comment.py b/image/src/github_pr_comment/comment.py index 61f788e0..281d35c5 100644 --- a/image/src/github_pr_comment/comment.py +++ b/image/src/github_pr_comment/comment.py @@ -230,8 +230,6 @@ def find_comment(github: GithubApi, issue_url: IssueUrl, username: str, headers: if comment_payload['user']['login'] != username: continue - #debug(json.dumps(comment_payload)) - if comment := _from_api_payload(comment_payload): if comment.headers: @@ -247,14 +245,24 @@ def find_comment(github: GithubApi, issue_url: IssueUrl, username: str, headers: # Match by description only if comment.description == legacy_description and backup_comment is None: - debug('Found backup comment that matches legacy description') + debug(f'Found backup comment that matches legacy description {comment.description=}') backup_comment = comment debug(f"Didn't match comment with {comment.description=}") if backup_comment is not None: debug('Found comment matching legacy description') - return backup_comment + + # Insert known headers into legacy comment + return TerraformComment( + issue_url=backup_comment.issue_url, + comment_url=backup_comment.comment_url, + headers={k: v for k, v in headers.items() if v is not None}, + description=backup_comment.description, + summary=backup_comment.summary, + body=backup_comment.body, + status=backup_comment.status + ) debug('No existing comment exists') return TerraformComment( From 1857bf934b41eaf421b45c6fa7f98fc927300125 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Mon, 2 May 2022 10:54:38 +0100 Subject: [PATCH 18/18] Don't print status update errors to workflow log --- image/actions.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/image/actions.sh b/image/actions.sh index ea850d92..e74e6c34 100644 --- a/image/actions.sh +++ b/image/actions.sh @@ -327,8 +327,10 @@ function output() { function update_status() { local status="$1" - if ! STATUS="$status" github_pr_comment status; then - echo + if ! STATUS="$status" github_pr_comment status 2>"$STEP_TMP_DIR/github_pr_comment.stderr"; then + debug_file "$STEP_TMP_DIR/github_pr_comment.stderr" + else + debug_file "$STEP_TMP_DIR/github_pr_comment.stderr" fi }