Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .github/actions/checkout_dependency/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# checkout_dependency

This action checks-out your repository under the specified destination directory using the action actions/checkout. Use the `depends-On: repository/pull/xx` to override the reference to checkout.

# Usage

<!-- start usage -->

```yaml
- uses: ansible-network/github_actions/.github/actions/checkout_dependency@main
with:
# Repository name with owner. For example, ansible-collections/kubernetes.core
repository: ""

# The branch, tag, or SHA to checkout when the pull request body does not
# contain any override for this repository.
ref: ""

# Relative path under $GITHUB_WORKSPACE to place the repository
path: ""

# Number of commits to fetch. 0 indicates all history for all branches and tags.
fetch-depth: "1"
```

<!-- end usage -->

# Depending on others PRs

The pull request body should contain the following sequence:

```
Depends-On: repository/pull/xx
```

# Scenarios

- [checkout pull request 12345 from repository my_org/my_collection](#Checkout-depending-pull-request)

## Checkout depending pull request

Github action step:

```yaml
- uses: ansible-network/github_actions/.github/actions/checkout_dependency@main
with:
repository: my_org/my_collection
ref: main
path: /path/to/checkout/repository
```

Pull request body:

```text
Depends-On: https://github.com/my_org/my_collection/pull/12345
```
48 changes: 48 additions & 0 deletions .github/actions/checkout_dependency/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: "Checkout Dependency"
description: |
checkout repository and override commit based on keyword 'depends-on' from pull request message
inputs:
repository:
description: "Repository name with owner. For example, ansible-collections/cloud.common"
default: ${{ github.repository }}
ref:
description: "The default branch, tag or SHA to checkout if no reference (using depends-on) is found"
path:
description: "Relative path under $GITHUB_WORKSPACE to place the repository"
fetch-depth:
description: "Number of commits to fetch. 0 indicates all history for all branches and tags."
default: "1"
runs:
using: composite
steps:
- name: Set up Python '3.9'
uses: actions/setup-python@v4
with:
python-version: "3.9"

- name: install PyGithub
run: |
pip install -U PyGithub
shell: bash

- id: resolve-dependency
shell: bash
run: |
python ${{ github.action_path }}/resolve_dependency.py
env:
RESOLVE_REF_PR_BODY: ${{ github.event.pull_request.body }}
RESOLVE_REF_REPOSITORY: ${{ inputs.repository }}

- name: Display reference to checkout
run: echo "Resolved reference -> '${RESOLVED_REF}'"
shell: bash
env:
RESOLVED_REF: ${{ steps.resolve-dependency.outputs.merge_commit_sha }}

- name: checkout repository
uses: actions/checkout@v3
with:
repository: ${{ inputs.repository }}
path: ${{ inputs.path }}
ref: ${{ steps.resolve-dependency.outputs.merge_commit_sha || inputs.ref }}
fetch-depth: ${{ inputs.fetch-depth }}
78 changes: 78 additions & 0 deletions .github/actions/checkout_dependency/resolve_dependency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/python
"""Script to check if a depends-on pull request has been defined into pull request body."""

import logging
import os
import re
import sys

from github import Github


FORMAT = "[%(asctime)s] - %(message)s"
logging.basicConfig(format=FORMAT)
logger = logging.getLogger("resolve_dependency")
logger.setLevel(logging.DEBUG)


def get_pr_merge_commit_sha(repository: str, pr_number: int) -> str:
"""Retrieve pull request merge commit sha.

:param repository: The repository name
:param pr_number: The pull request number
:returns: The pull request merge commit sha if it exists
:raises ValueError: if the pull request is not mergeable
"""
access_token = os.environ.get("GITHUB_TOKEN")
gh_obj = Github(access_token)
repo = gh_obj.get_repo(repository)

pr_obj = repo.get_pull(pr_number)
if not pr_obj.mergeable:
# raise an error when the pull request is not mergeable
sys.tracebacklimit = -1
raise ValueError(f"Pull request {pr_number} from {repository} is not mergeable")

return pr_obj.merge_commit_sha


def resolve_ref(pr_body: str, repository: str) -> int:
"""Get pull request reference number defined with Depends-On.

:param pr_body: the pull request body
:param repository: The repository name
:returns: pull request number if it is defined else 0
"""
pr_regx = re.compile(
rf"^Depends-On:[ ]*https://github.com/{repository}/pull/(\d+)\s*$",
re.MULTILINE | re.IGNORECASE,
)
# Search for expression starting with depends-on not case-sensitive
match = pr_regx.search(pr_body)
return int(match.group(1)) if match else 0


def main() -> None:
"""Run the script."""
pr_body = os.environ.get("RESOLVE_REF_PR_BODY") or ""
repository = os.environ.get("RESOLVE_REF_REPOSITORY") or ""

if not repository:
return

pr_number = resolve_ref(pr_body, repository)
if not pr_number:
return
logger.info("Override checkout with pr number: %d", pr_number)

# get pull request merge commit sha
merge_commit_sha = get_pr_merge_commit_sha(repository, pr_number)
logger.info("merge commit sha for pull request %d => '%s'", pr_number, merge_commit_sha)
github_output = os.environ.get("GITHUB_OUTPUT")
if github_output:
with open(str(github_output), "a", encoding="utf-8") as file_handler:
file_handler.write(f"merge_commit_sha={merge_commit_sha}\n")


if __name__ == "__main__":
main()
158 changes: 158 additions & 0 deletions .github/actions/checkout_dependency/test_resolve_dependency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#!/usr/bin/env python3
"""Module used to test resolve_dependency.py script."""

import os
import string

from pathlib import PosixPath
from random import choice
from unittest.mock import MagicMock
from unittest.mock import patch

import pytest

from resolve_dependency import get_pr_merge_commit_sha
from resolve_dependency import main
from resolve_dependency import resolve_ref


@pytest.mark.parametrize(
"pr_body,match",
[
("Depends-On: https://github.com/my_org/my_collection/pull/12345", True),
(
"Depends-On: https://github.com/my_org/my_collection/pull/12345\n"
"Depends-On: https://github.com/my_org/my_collection/pull/67890",
True,
),
(
"Depends-On: https://github.com/another_org/my_collection/pull/4000\n"
"Depends-On: https://github.com/my_org/my_collection/pull/12345",
True,
),
(
"Depends-On: https://github.com/my_org/my_collection/pull/12345\n"
"Depends-On: https://github.com/my_org/my_collection/pull/67890",
True,
),
("Depends-On: https://github.com/another_org/my_collection/pull/12345", False),
("Depends-On: https://github.com/my_org/my_collection2/pull/12345", False),
("Depends-On: https://github.com/my_org/my_collection/pull", False),
],
)
def test_resolve_ref(pr_body: str, match: bool) -> None:
"""Test resolve_ref function.

:param pr_body: pull request body
:param match: whether a depends-on should be found or not
"""
expected = 12345 if match else 0
assert resolve_ref(pr_body, "my_org/my_collection") == expected


class FakePullRequest:
# pylint: disable=too-few-public-methods
"""Class to simulate PullRequest Object."""

def __init__(self, mergeable: bool) -> None:
"""Class constructor.

:param mergeable: whether the pull request is mergeable or not
"""
self.mergeable = mergeable
self.merge_commit_sha = self.generate_commit_sha()

@staticmethod
def generate_commit_sha(length: int = 16) -> str:
"""Generate random commit sha.

:param length: The length of the generated string
:returns: The generated commit sha
"""
data = string.ascii_letters + string.digits
return "".join([choice(data) for _ in range(length)])


@pytest.mark.parametrize("mergeable", [True, False])
@patch("resolve_dependency.Github")
def test_get_pr_merge_commit_sha(m_github: MagicMock, mergeable: bool) -> None:
"""Test get_pr_merge_commit_sha function.

:param m_github: The github module
:param mergeable: whether the pull request is mergeable or not
"""
github_obj = MagicMock()
m_github.return_value = github_obj

os.environ["GITHUB_TOKEN"] = "unittest_github_token"

m_github_repo = MagicMock()
github_obj.get_repo = MagicMock()
github_obj.get_repo.return_value = m_github_repo

local_pr = FakePullRequest(mergeable=mergeable)
m_github_repo.get_pull = MagicMock()
m_github_repo.get_pull.return_value = local_pr

repository = "my_testing_repository"
pr_number = 12345

if mergeable:
assert get_pr_merge_commit_sha(repository, pr_number) == local_pr.merge_commit_sha
else:
with pytest.raises(ValueError):
get_pr_merge_commit_sha(repository, pr_number)

m_github.assert_called_once_with("unittest_github_token")
github_obj.get_repo.assert_called_once_with(repository)
m_github_repo.get_pull.assert_called_once_with(pr_number)


@pytest.mark.parametrize("repository", [True, False])
@pytest.mark.parametrize("resolve_ref_pr", [0, 1])
@patch("resolve_dependency.get_pr_merge_commit_sha")
@patch("resolve_dependency.resolve_ref")
def test_main(
m_resolve_ref: MagicMock,
m_get_pr_merge_commit_sha: MagicMock,
repository: bool,
resolve_ref_pr: int,
tmp_path: PosixPath,
) -> None:
"""Test main function.

:param m_resolve_ref: The resolve_ref mock function
:param m_get_pr_merge_commit_sha: The get_pr_merge_commit_sha mock function
:param repository: whether the repository is defined on environment variable or not
:param resolve_ref_pr: The pull request number
:param tmp_path: The temporary path for file to create for test
"""
pr_body = "My pull request body - this is a sample for unit tests"
repository_name = "my_test_repository"
os.environ["RESOLVE_REF_PR_BODY"] = pr_body

gh_output_file = tmp_path / "github_output.txt"
env_update = {"GITHUB_OUTPUT": str(gh_output_file)}
if repository:
env_update.update({"RESOLVE_REF_REPOSITORY": repository_name})

m_resolve_ref.return_value = resolve_ref_pr
merge_commit_sha = FakePullRequest.generate_commit_sha()
m_get_pr_merge_commit_sha.return_value = merge_commit_sha

with patch.dict(os.environ, env_update):
main()

if not repository:
m_resolve_ref.assert_not_called()
m_get_pr_merge_commit_sha.assert_not_called()
assert not gh_output_file.exists()
elif not resolve_ref_pr:
m_resolve_ref.assert_called_once_with(pr_body, repository_name)
m_get_pr_merge_commit_sha.assert_not_called()
assert not gh_output_file.exists()
else:
m_resolve_ref.assert_called_once_with(pr_body, repository_name)
m_get_pr_merge_commit_sha.assert_called_once_with(repository_name, resolve_ref_pr)
assert gh_output_file.exists()
# gh_output_file.read_text() == f"merge_commit_sha={merge_commit_sha}\n"
2 changes: 1 addition & 1 deletion .github/workflows/ansible-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ jobs:
ansible-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ansible-network/github_actions/.github/actions/checkout_dependency@main
- name: Run ansible-lint
uses: ansible-community/ansible-lint-action@main
2 changes: 1 addition & 1 deletion .github/workflows/coverage_network_devices.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
name: "Code Coverage | Python 3.10"
steps:
- name: Checkout the collection repository
uses: actions/checkout@v3
uses: ansible-network/github_actions/.github/actions/checkout_dependency@main
with:
path: ${{ env.source_directory }}
ref: ${{ github.event.pull_request.head.sha }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:
name: "py${{ matrix.python-version }} / ${{ matrix.ansible-version }} / ${{ matrix.dependency-source }}"
steps:
- name: Checkout the collection repository
uses: actions/checkout@v3
uses: ansible-network/github_actions/.github/actions/checkout_dependency@main
with:
path: ${{ env.source_directory }}
ref: ${{ github.event.pull_request.head.sha }}
Expand All @@ -79,7 +79,7 @@ jobs:

- name: Checkout ${{ steps.identify.outputs.dependency }}
if: steps.identify.outputs.dependency != '' && matrix.dependency-source == 'github'
uses: actions/checkout@v3
uses: ansible-network/github_actions/.github/actions/checkout_dependency@main
with:
repository: ansible-collections/${{ steps.identify.outputs.dependency }}
path: ${{ env.dependency_directory }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/integration_simple.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ jobs:
name: "py${{ matrix.python-version }} / ${{ matrix.ansible-version }}"
steps:
- name: Checkout the collection repository
uses: actions/checkout@v3
uses: ansible-network/github_actions/.github/actions/checkout_dependency@main
with:
path: ${{ env.source_directory }}
ref: ${{ github.event.pull_request.head.sha }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/sanity.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ jobs:
name: "py${{ matrix.python-version }} / ${{ matrix.os }} / ${{ matrix.ansible-version }}"
steps:
- name: Checkout the collection repository
uses: actions/checkout@v3
uses: ansible-network/github_actions/.github/actions/checkout_dependency@main
with:
path: ${{ env.source_directory }}
ref: ${{ github.event.pull_request.head.sha }}
Expand Down
Loading