Skip to content
This repository has been archived by the owner on Aug 10, 2023. It is now read-only.

Commit

Permalink
Add some options for more control
Browse files Browse the repository at this point in the history
- Option to choose the checks that needs to be performed.
- Option to define a delay for the build and app load stages.
- Plus some code improvements and docs update.
  • Loading branch information
sayanarijit committed Nov 12, 2019
1 parent f85d33f commit b2d6db7
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 48 deletions.
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ FROM python:3.7-alpine
RUN pip install --upgrade pip
RUN pip install requests==2.22.0

COPY review_app_status.py /
WORKDIR /app
COPY . .

ENTRYPOINT ["python3", "/review_app_status.py"]
CMD ["python3", "/app/review_app_status.py"]
52 changes: 46 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
A Github Action that tests the deployment status of a Heroku Review App.


## Usage:
## Usage
* Include the action in the workflow
```yaml
name: Review App Test
Expand All @@ -22,13 +22,16 @@ A Github Action that tests the deployment status of a Heroku Review App.

steps:
- name: Run review-app test
uses: niteoweb/reviewapps-deploy-status@v1.0.2
uses: niteoweb/reviewapps-deploy-status@v1.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
interval: 10 # in seconds, optional, default is 10
accepted_responses: 200 # comma separated status codes, optional, default is 200
deployments_timeout: 120 # in seconds, optional, default is 120
checks: build, response # check the build status and if the app is responding properly
build_time_delay: 5 # delay the checks till the app is built, default is 5 seconds
load_time_delay: 5 # delay the checks till the app is loaded after build, default is 5 seconds
interval: 10 # interval to repeat checks, default is 10 seconds
accepted_responses: 200 # comma separated status codes, optional, default is 200
deployments_timeout: 120 # in seconds, optional, default is 120
```
> Note: Work flow should include `pull_request` event.
Expand All @@ -39,10 +42,47 @@ A Github Action that tests the deployment status of a Heroku Review App.

| Name | Description | Default |
|---|---|---|
| checks | Comma separated list of checks to be performed | All checks: build, response |
| build_time_delay | Delay for the build stage of the review app | 5 |
| load_time_delay | Delay for the app to load and start serving after it is built | 5 |
| interval | Wait for this amount of seconds before retrying the build check | 10 |
| accepted_responses | Allow/Accept the specified status codes | 200 |
| accepted_responses | Allow/Accept the specified status codes (comma separated) | 200 |
| deployments_timeout | Maximum waiting time (in seconds) to fetch the deployments | 120 |


## Workflow

```
Initialize
├── Build time delay
├── Fetch build data
├── Is `build` check included in the `checks`?
│ ├── Yes
│ │ └── Is the build status a `success`?
│ │ ├── Yes
│ │ │ └── Continue
│ │ └── No
│ │ └── Are we past the `deployments_timeout`?
│ │ ├── Yes
│ │ │ └── Fail
│ │ └── No
│ │ └── Repeat from `Fetch build data`
│ └── No
│ └── Continue
├── Load time delay
├── Is `response` check included in the `checks`?
│ ├── Yes
│ │ ├── Do an HTTP request to the app URL.
│ │ └── Is the HTTP response in the `accepted_responses`?
│ │ ├── Yes
│ │ │ └── Continue
│ │ └── No
│ │ └── Fail
│ └── No
│ └── Continue
└── Done (success)
```
## Local Development
* Create a Python virtual environment(version > 3.6).
* Activate the environment.
Expand Down
23 changes: 18 additions & 5 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,28 @@ runs:
image: 'Dockerfile'

inputs:
checks:
description: Comma separated list of checks to perform.
required: false
# Default: Perform all checks
default: build, response
build_time_delay:
description: Delay for the build stage of the review app.
required: false
default: 5
load_time_delay:
description: Delay for the app to load and start serving after it is built.
required: false
default: 5
interval:
description: Interval to check the status.
description: Interval to repeat the deployment check in seconds.
required: false
default: 10 # 10 seconds
default: 10
accepted_responses:
description: Status(es) which can be accepted. Separated by comma.
description: Comma separated status which can be accepted.
required: false
default: 200 # All OK status
default: 200
deployments_timeout:
description: Maximum waiting time to fetch the deployments.
required: false
default: 120 # 120 seconds
default: 120
119 changes: 93 additions & 26 deletions review_app_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,56 @@
import logging
import os
import time
from enum import Enum
import typing as t
from dataclasses import dataclass
from enum import Enum, auto

import requests

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("review_app_status")


SUCCESS = "success" # Review App Success Build state
class BuildStates(Enum):
"""Expected reviewapp app build states"""

success = "success"

def _make_github_api_request(url):

class Checks(Enum):
"""Available checks"""

# Check if the build was success
build = auto()

# Check the HTTP response from app URL
response = auto()


@dataclass(frozen=True)
class Args:
"""User input arguments"""

# Checks to be performed
checks: t.List[Checks]

# Delay for the application to be built in Heroku
build_time_delay: int

# Delay for the application to load and start serving
load_time_delay: int

# Interval between the repeating checks
interval: int

# Acceptable responses for the response check
accepted_responses: t.List[int]

# Max time spend retrying for the build check.
deployments_timeout: t.List[int]


def _make_github_api_request(url: str) -> dict:
"""Make github API request with `deployment` event specific headers.
Input:
Expand All @@ -33,7 +71,9 @@ def _make_github_api_request(url):
return r.json()


def _get_github_deployment_status_url(deployments_url, commit_sha, timeout, interval):
def _get_github_deployment_status_url(
deployments_url: str, commit_sha: str, timeout: int, interval: int
) -> None:
"""Get deployment_status URL for the head commit.
Inputs:
deployments_url: This can be obtained from `pull_request` event payload.
Expand All @@ -60,7 +100,7 @@ def _get_github_deployment_status_url(deployments_url, commit_sha, timeout, inte
raise ValueError("No deployment found for the lastest commit.")


def _get_build_data(url, interval):
def _get_build_data(url: str, interval: int) -> dict:
"""Get Review App build data using Github's `deployment_status` API.
Inputs:
Expand Down Expand Up @@ -92,7 +132,9 @@ def _get_build_data(url, interval):
return response[0]


def _check_review_app_deployment_status(review_app_url, accepted_responses):
def _check_review_app_deployment_status(
review_app_url: str, accepted_responses: t.List[int]
):
"""Check Review App deployment status code against accepted_responses.
Inputs:
Expand All @@ -107,38 +149,63 @@ def _check_review_app_deployment_status(review_app_url, accepted_responses):
r.raise_for_status()


def main():
def main() -> None:
"""Main workflow.
All the inputs are received from workflow as environment variables.
"""
interval_arg = int(os.environ["INPUT_INTERVAL"])
deployments_timeout_arg = int(os.environ["INPUT_DEPLOYMENTS_TIMEOUT"])
accepted_responses_arg = os.environ["INPUT_ACCEPTED_RESPONSES"]
event_payload_path = os.environ["GITHUB_EVENT_PATH"]

logger.info(f"Statuses being accepted: {accepted_responses_arg}")
accepted_responses = set(map(int, accepted_responses_arg.split(",")))
args = Args(
checks=[Checks[x.strip()] for x in os.environ["INPUT_CHECKS"].split(",")],
build_time_delay=int(os.environ["INPUT_BUILD_TIME_DELAY"]),
load_time_delay=int(os.environ["INPUT_LOAD_TIME_DELAY"]),
interval=int(os.environ["INPUT_INTERVAL"]),
deployments_timeout=int(os.environ["INPUT_DEPLOYMENTS_TIMEOUT"]),
accepted_responses=[
int(x.strip()) for x in os.environ["INPUT_ACCEPTED_RESPONSES"].split(",")
],
)

# Delay the checks till the app is built
logger.info(f"Build time delay: {args.build_time_delay} seconds")
time.sleep(args.build_time_delay)

with open(event_payload_path) as f:
data = f.read()
pull_request_data = json.loads(data)
logger.info(f"Statuses being accepted: {args.accepted_responses}")

with open(os.environ["GITHUB_EVENT_PATH"]) as f:
pull_request_data = json.load(f)

# Fetch the GitHub status URL
github_deployment_status_url = _get_github_deployment_status_url(
pull_request_data["repository"]["deployments_url"],
pull_request_data["pull_request"]["head"]["sha"],
deployments_timeout_arg,
interval_arg,
deployments_url=pull_request_data["repository"]["deployments_url"],
commit_sha=pull_request_data["pull_request"]["head"]["sha"],
timeout=args.deployments_timeout,
interval=args.interval,
)

reviewapp_build_data = _get_build_data(github_deployment_status_url, interval_arg)

if reviewapp_build_data["state"] != SUCCESS:
raise ValueError(f"Review App Build state: {reviewapp_build_data['state']}")
# Fetch other build data
reviewapp_build_data = _get_build_data(
url=github_deployment_status_url, interval=args.interval
)

review_app_url = f"https://{reviewapp_build_data['environment']}.herokuapp.com"
# Perform all the checks now

_check_review_app_deployment_status(review_app_url, accepted_responses)
if Checks.build in args.checks:
# Check if the build was success
build_state = reviewapp_build_data["state"]
if build_state != BuildStates.success.value:
raise ValueError(f"Review App Build state: {build_state}")

if Checks.response in args.checks:
# Delay the checks till the app is loads
logger.info(f"Load time delay: {args.load_time_delay} seconds")
time.sleep(args.load_time_delay)

# Check the HTTP response from app URL
review_app_url = f"https://{reviewapp_build_data['environment']}.herokuapp.com"
_check_review_app_deployment_status(
review_app_url=review_app_url, accepted_responses=args.accepted_responses
)

print("Successful")

Expand Down
43 changes: 34 additions & 9 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ def test_get_deployment_status_interval_greater_failure(mock_github_request):

with pytest.raises(ValueError) as excinfo:
url = _get_github_deployment_status_url(
"https://foo.bar/deployments", "commitsha12345", 3, 4
deployments_url="https://foo.bar/deployments",
commit_sha="commitsha12345",
timeout=3,
interval=4,
)
assert "Interval can't be greater than deployments_timeout." in str(excinfo.value)

Expand All @@ -77,7 +80,10 @@ def test_get_deployment_status_url_success(mock_github_request):
}
]
url = _get_github_deployment_status_url(
"https://foo.bar/deployments", "commitsha12345", 2, 1
deployments_url="https://foo.bar/deployments",
commit_sha="commitsha12345",
timeout=2,
interval=1,
)
assert url == "https://foo.bar/deployment/statuses/1"
mock_github_request.assert_called_once_with("https://foo.bar/deployments")
Expand All @@ -93,7 +99,10 @@ def test_get_deployment_status_url_failure(mock_github_request, caplog):
]
with pytest.raises(ValueError) as excinfo:
url = _get_github_deployment_status_url(
"https://foo.bar/deployments", "commitsha12345", 2, 1
deployments_url="https://foo.bar/deployments",
commit_sha="commitsha12345",
timeout=2,
interval=1,
)

assert (
Expand Down Expand Up @@ -154,7 +163,7 @@ def test_get_one_build_data_status(mock_github_request):
def test_get_pending_build_data_status(mock_github_request, caplog):
from review_app_status import _get_build_data

data = _get_build_data("https://foo.bar/deployments/1/status", 1)
data = _get_build_data(url="https://foo.bar/deployments/1/status", interval=1)
assert data == {"id": "1"}
assert (
caplog.records[0].message
Expand Down Expand Up @@ -230,6 +239,9 @@ def test_check_review_app_custom_status_success(caplog):
@mock.patch.dict(
os.environ,
{
"INPUT_CHECKS": "build, response",
"INPUT_BUILD_TIME_DELAY": "5",
"INPUT_LOAD_TIME_DELAY": "5",
"INPUT_DEPLOYMENTS_TIMEOUT": "20",
"INPUT_INTERVAL": "10",
"INPUT_ACCEPTED_RESPONSES": "200, 302",
Expand Down Expand Up @@ -262,11 +274,16 @@ def test_main_success(

mock_file.assert_called_with("./test_path")
mock_deployment_status_url.assert_called_once_with(
"http://foo.bar/deployments", "commit12345", 20, 10
deployments_url="http://foo.bar/deployments",
commit_sha="commit12345",
timeout=20,
interval=10,
)
mock_build_data.assert_called_once_with(
url="http://foo.bar/deployment_status", interval=10
)
mock_build_data.assert_called_once_with("http://foo.bar/deployment_status", 10)
mock_review_app_deployment.assert_called_once_with(
"https://foo-pr-bar.herokuapp.com", {200, 302}
review_app_url="https://foo-pr-bar.herokuapp.com", accepted_responses=[200, 302]
)

out, err = capsys.readouterr()
Expand All @@ -276,6 +293,9 @@ def test_main_success(
@mock.patch.dict(
os.environ,
{
"INPUT_CHECKS": "build, response",
"INPUT_BUILD_TIME_DELAY": "5",
"INPUT_LOAD_TIME_DELAY": "5",
"INPUT_DEPLOYMENTS_TIMEOUT": "20",
"INPUT_INTERVAL": "10",
"INPUT_ACCEPTED_RESPONSES": "200, 302",
Expand Down Expand Up @@ -305,9 +325,14 @@ def test_main_failure(

mock_file.assert_called_with("./test_path")
mock_deployment_status_url.assert_called_once_with(
"http://foo.bar/deployments", "commit12345", 20, 10
deployments_url="http://foo.bar/deployments",
commit_sha="commit12345",
timeout=20,
interval=10,
)
mock_build_data.assert_called_once_with(
url="http://foo.bar/deployment_status", interval=10
)
mock_build_data.assert_called_once_with("http://foo.bar/deployment_status", 10)

"Review App Build state: failure" in str(excinfo.value)
mock_review_app_deployment.assert_not_called()

0 comments on commit b2d6db7

Please sign in to comment.