Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Task: github_org_to_env #3843

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
20 changes: 16 additions & 4 deletions cumulusci/core/config/sfdx_org_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ class SfdxOrgConfig(OrgConfig):
def sfdx_info(self):
if hasattr(self, "_sfdx_info"):
return self._sfdx_info
self._sfdx_info = self.get_sfdx_info()
return self._sfdx_info

def get_sfdx_info(self, verbose: bool = False):
# On-demand creation of scratch orgs
if self.createable and not self.created:
self.create_org()
Expand All @@ -29,7 +32,11 @@ def sfdx_info(self):

# Call force:org:display and parse output to get instance_url and
# access_token
p = sfdx("force:org:display --json", self.username)
command = f"force:org:display --json"
if verbose:
self.logger.warning("Using --verbose mode to retrieve the sfdxAuthUrl")
command += " --verbose"
p = sfdx(command, self.username)

org_info = None
stderr_list = [line.strip() for line in p.stderr_text]
Expand Down Expand Up @@ -63,16 +70,21 @@ def sfdx_info(self):
}
if org_info["result"].get("password"):
sfdx_info["password"] = org_info["result"]["password"]
self._sfdx_info = sfdx_info
self._sfdx_info_date = datetime.datetime.utcnow()
self.config.update(sfdx_info)
if not verbose:
self._sfdx_info = sfdx_info
self._sfdx_info_date = datetime.datetime.utcnow()
self.config.update(sfdx_info)

sfdx_info.update(
{
"created_date": org_info["result"].get("createdDate"),
"expiration_date": org_info["result"].get("expirationDate"),
}
)

if verbose:
# Add the sfdx_auth_url to output but don't store in org config
sfdx_info["sfdx_auth_url"] = org_info["result"].get("sfdxAuthUrl")
return sfdx_info

@property
Expand Down
52 changes: 52 additions & 0 deletions cumulusci/core/config/tests/test_config_expensive.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,58 @@ def test_scratch_info_username_not_found(self, Command):
with pytest.raises(ScratchOrgException):
config.scratch_info

def test_scratch_info_verbose(self, Command, caplog):
result = b"""{
"result": {
"instanceUrl": "url",
"accessToken": "access!token",
"username": "username",
"password": "password",
"sfdxAuthUrl": "test_auth_url",
"createdDate": "1970-01-01T00:00:00Z",
"expirationDate": "1970-01-08"
}
}"""
Command.return_value = mock.Mock(
stderr=io.BytesIO(b""), stdout=io.BytesIO(result), returncode=0
)

config = ScratchOrgConfig({"username": "test", "created": True}, "test")

expected = {
"access_token": "access!token",
"instance_url": "url",
"org_id": "access",
"password": "password",
"username": "username",
"sfdx_auth_url": "test_auth_url",
"created_date": "1970-01-01T00:00:00Z",
"expiration_date": "1970-01-08",
}
with caplog.at_level("WARNING"):
sfdx_info = config.get_sfdx_info(verbose=True)
assert "sfdx_auth_url" in sfdx_info
assert sfdx_info["sfdx_auth_url"] == "test_auth_url"

# Ensure that the verbose sfdx_info is not stored in the org config
assert hasattr(config, "_sfdx_info") is False

# Check the command that was passed to the mocked Command
Command.assert_called_with(
"sfdx force:org:display --json --verbose -u test",
stdout=mock.ANY,
stderr=mock.ANY,
shell=True,
env=mock.ANY,
)

logs = [
record.message for record in caplog.records if record.levelname == "WARNING"
]
logs = "\n".join(logs)
# Assert that the log warning message was emitted about accessing sfdxAuthUrl
assert "Using --verbose mode to retrieve the sfdxAuthUrl" in logs

def test_access_token(self, Command):
config = ScratchOrgConfig({}, "test")
_marker = object()
Expand Down
10 changes: 10 additions & 0 deletions cumulusci/core/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ class OrgCannotBeLoaded(CumulusCIUsageError):
pass


class OrgNotValidForTask(CumulusCIUsageError):
"""Raised when an org is not valid for a task"""

pass


class ServiceNotConfigured(CumulusCIUsageError):
"""Raised when no service configuration could be found by a given name in the project keychain"""

Expand Down Expand Up @@ -212,6 +218,10 @@ class GithubApiUnauthorized(CumulusCIException):
pass


class GithubEnvironmentError(CumulusCIException):
pass


class SalesforceException(CumulusCIException):
"""Raise for errors related to Salesforce"""

Expand Down
12 changes: 11 additions & 1 deletion cumulusci/core/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import re
import time
import webbrowser
from base64 import b64encode
from string import Template
from typing import Callable, Optional, Union
from urllib.parse import urlparse
Expand All @@ -22,6 +23,7 @@
from github3.repos.release import Release
from github3.repos.repo import Repository
from github3.session import GitHubSession
from nacl import encoding, public
from requests.adapters import HTTPAdapter
from requests.exceptions import RetryError
from requests.models import Response
Expand Down Expand Up @@ -603,7 +605,7 @@ def catch_common_github_auth_errors(func: Callable) -> Callable:
def inner(*args, **kwargs):
try:
return func(*args, **kwargs)
except (ConnectionError) as exc:
except ConnectionError as exc:
if error_msg := format_github3_exception(exc):
raise GithubApiError(error_msg) from exc
else:
Expand Down Expand Up @@ -663,3 +665,11 @@ def create_gist(github, description, files):
files - A dict of files in the form of {filename:{'content': content},...}
"""
return github.create_gist(description, files, public=False)


def encrypt_secret(public_key: str, secret_value: str) -> str:
"""Encrypt a Unicode string for GitHub Secrets using the public key."""
public_key = public.PublicKey(public_key.encode("utf-8"), encoding.Base64Encoder())
sealed_box = public.SealedBox(public_key)
encrypted = sealed_box.encrypt(secret_value.encode("utf-8"))
return b64encode(encrypted).decode("utf-8")
26 changes: 26 additions & 0 deletions cumulusci/core/tests/test_github.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import base64
import json
import os
from datetime import datetime
Expand All @@ -18,6 +19,7 @@
from github3.pulls import ShortPullRequest
from github3.repos.repo import Repository
from github3.session import AppInstallationTokenAuth
from nacl import encoding, public
from requests.exceptions import RequestException, RetryError, SSLError
from requests.models import Response

Expand Down Expand Up @@ -909,3 +911,27 @@ def test_get_latest_prerelease(self, base_url, endpoint):
get_latest_prerelease(repo=repo)
assert responses.assert_call_count(endpoint, 1)
repo.release_from_tag.assert_called_once_with(expected_tag)


def test_encrypt_secret():
# Generate a real public/private key pair
private_key = public.PrivateKey.generate()
public_key = private_key.public_key

# Convert the public key to a base64-encoded string
public_key_str = public_key.encode(encoder=encoding.Base64Encoder()).decode("utf-8")

# Secret value to encrypt
secret_value = "test_secret"

# Encrypt the secret using the encrypt_secret function
encrypted_secret = github.encrypt_secret(public_key_str, secret_value)

# Decrypt the secret to verify it was encrypted correctly
sealed_box = public.SealedBox(private_key)
decrypted_secret = sealed_box.decrypt(base64.b64decode(encrypted_secret)).decode(
"utf-8"
)

# Assert that the decrypted secret matches the original secret value
assert decrypted_secret == secret_value
4 changes: 4 additions & 0 deletions cumulusci/cumulusci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,10 @@ tasks:
description: Copies one or more subtrees from the project repository for a given release to a target repository, with the option to include release notes.
class_path: cumulusci.tasks.github.publish.PublishSubtree
group: GitHub
github_org_to_env:
description: Publishes an org's credentials and info as a GitHub Environment. This task will create or update an environment with the same ORG_ID value automatically.
class_path: cumulusci.tasks.github.environments.OrgToEnvironment
group: GitHub
github_package_data:
description: Look up 2gp package dependencies for a version id recorded in a commit status.
class_path: cumulusci.tasks.github.commit_status.GetPackageDataFromCommitStatus
Expand Down
Loading
Loading