From 2c59cbfacdcfef860610018ebc99027013ac10d8 Mon Sep 17 00:00:00 2001 From: Nafis Bey Date: Tue, 17 Aug 2021 10:36:17 -0400 Subject: [PATCH 1/3] feat: implement groups network api endpoint --- php-classes/Slate/NetworkHub/Connector.php | 363 +++++++++++------- php-classes/Slate/NetworkHub/School.php | 77 +++- php-classes/Slate/NetworkHub/SchoolUser.php | 14 +- .../Slate/NetworkHub/SchoolUserMapping.php | 43 +++ php-classes/Slate/NetworkHub/User.php | 23 +- 5 files changed, 376 insertions(+), 144 deletions(-) create mode 100644 php-classes/Slate/NetworkHub/SchoolUserMapping.php diff --git a/php-classes/Slate/NetworkHub/Connector.php b/php-classes/Slate/NetworkHub/Connector.php index b431fc1..4cb463b 100644 --- a/php-classes/Slate/NetworkHub/Connector.php +++ b/php-classes/Slate/NetworkHub/Connector.php @@ -12,6 +12,7 @@ use Emergence\KeyedDiff; use Emergence\People\IUser; use Emergence\Util\Url as UrlUtil; +use Emergence\EventBus; use Psr\Log\LoggerInterface; @@ -21,7 +22,8 @@ class Connector extends AbstractConnector implements ISynchronize public static $connectorId = 'network-hub'; public static $apiEndpoints = [ - 'users' => '/network-api/users' + 'users' => '/network-api/users', + 'groups' => '/network-api/groups' ]; public static function handleRequest($action = null) @@ -45,20 +47,21 @@ public static function handleNetworkLoginRequest($returnUrl = false) $NetworkSchools = []; if ($NetworkUser) { - $NetworkSchools = School::getAllByWhere([ - 'ID' => [ - 'operator' => 'IN', - 'values' => [ - array_keys(SchoolUser::getAllByWhere([ - 'PersonID' => $NetworkUser->ID - ], [ - 'indexField' => 'SchoolID' - ])) + $schoolIds = array_keys(SchoolUser::getAllByWhere([ + 'PersonID' => $NetworkUser->ID + ], [ + 'indexField' => 'SchoolID' + ])); + if (!empty($schoolIds)) { + $NetworkSchools = School::getAllByWhere([ + 'ID' => [ + 'operator' => 'IN', + 'values' => $schoolIds ] - ] - ],[ - 'indexField' => 'Handle' - ]); + ],[ + 'indexField' => 'Handle' + ]); + } } // error out if... if ( @@ -90,6 +93,12 @@ public static function handleNetworkLoginRequest($returnUrl = false) $NetworkSchool = reset($NetworkSchools); } + EventBus::fireEvent('networkUserLogIn', static::class, [ + 'School' => $NetworkSchool, + 'User' => $NetworkUser, + 'requestData' => $_REQUEST + ]); + $queryParameters = http_build_query([ 'username' => $NetworkUser->Email, 'redirectUrl' => UrlUtil::buildAbsolute($_REQUEST['_LOGIN']['return']) @@ -116,6 +125,7 @@ protected static function _getJobConfig(array $requestData) $config['pullUsers'] = !empty($requestData['pullUsers']); $config['schools'] = !empty($requestData['schools']) ? $requestData['schools'] : []; + $config['groupList'] = !empty($requestData['groups']) ? $requestData['groups'] : []; return $config; } @@ -161,8 +171,19 @@ public static function synchronize(IJob $Job, $pretend = true) $results['created'][$NetworkHubSchool->Domain] = $syncResults['created']; $results['created']['total'] += $syncResults['created']; - $results['skipped'][$NetworkHubSchool->Domain] = $syncResults['skipped']; $results['skipped']['total'] += $syncResults['skipped']; + /* Uncomment for debug */ + $results['skipped'][$NetworkHubSchool->Domain] = [ + // 'usernames' => array_map( + // function($r) { + // return $r['Username']; + // }, $syncResults['skippedUsers'] + // ), + 'usernames' => $syncResults['skippedReasons'], + // 'users' => $syncResults['skippedUsers'] + 'total' => $syncResults['skipped'] + ]; + $results['updated'][$NetworkHubSchool->Domain] = $syncResults['updated']; $results['updated']['total'] += $syncResults['updated']; @@ -194,8 +215,12 @@ public static function pullNetworkUsers(IJob $Job, School $NetworkSchool, $prete 'skipped' => 0, 'skippedUsers' => [] ]; + try { - $networkUsers = $NetworkSchool->fetchNetworkUsers(); + + $groupList = $Job->config['groupList']; + $networkUsers = $NetworkSchool->fetchNetworkGroupMembers($groupList); + foreach ($networkUsers as $networkUser) { try { $syncResults = static::syncNetworkUser($NetworkSchool, $networkUser, $Job, $pretend); @@ -207,7 +232,8 @@ public static function pullNetworkUsers(IJob $Job, School $NetworkSchool, $prete } elseif ($syncResults->getStatus() === SyncResult::STATUS_VERIFIED) { $results['verified']++; } elseif ($syncResults->getStatus() === SyncResult::STATUS_SKIPPED) { - $results['skippedUsers'][] = $networkUser; + // $results['skippedUsers'][] = $networkUser; + $results['skippedReasons'][$networkUser['Username']] = $syncResults->getInterpolatedMessage(); $results['skipped']++; } } catch (SyncException $s) { @@ -226,61 +252,62 @@ public static function syncNetworkUser(School $NetworkSchool, array $networkUser $logger = static::getLogger($logger); // skip slate users - if (empty($networkUserRecord['PrimaryEmail']['Data'])) { - return new SyncResult( - SyncResult::STATUS_SKIPPED, - 'Skipped {slateUsername} @ {slateDomain} due to missing PrimaryEmail', - [ - 'slateUsername' => $networkUserRecord['Username'], - 'slateDomain' => $NetworkSchool->Domain - ] - ); - } elseif (empty($networkUserRecord['Username'])) { - return new SyncResult( - SyncResult::STATUS_SKIPPED, - 'Skipped {slateUsername} @ {slateDomain} due to missing Slate username', - [ - 'slateUsername' => $networkUserRecord['PrimaryEmail']['Data'], - 'slateDomain' => $NetworkSchool->Domain - ] - ); - } elseif (empty($networkUserRecord['AccountLevel']) || $networkUserRecord['AccountLevel'] === 'Disabled') { - return new SyncResult( - SyncResult::STATUS_SKIPPED, - 'Skipped {slateUsername} @ {slateDomain} due to AccountLevel = Disabled', - [ - 'slateUsername' => $networkUserRecord['PrimaryEmail']['Data'], - 'slateDomain' => $NetworkSchool->Domain - ] - ); + if ($reasonToSkipSync = static::networkUserShouldNotBeSynced($networkUserRecord, $NetworkSchool)) { + return $reasonToSkipSync; } - $networkUserConditions = [ - 'Email' => $networkUserRecord['PrimaryEmail']['Data'] - ]; + // user exists, lets update their information + if ($NetworkUserMapping = SchoolUserMapping::getByExternalIdentifier($NetworkSchool, $networkUserRecord['ID'])) { + $NetworkUser = $NetworkUserMapping->Context->Person; - $NetworkUser = Person::getByWhere($networkUserConditions); + if (!$NetworkUser) { - // skip disabled hub users - if ($NetworkUser->AccountLevel === 'Disabled') { - return new SyncResult( - SyncResult::STATUS_SKIPPED, - 'Skipped updating {slateUsername} @ {slateDomain} due to AccountLevel = Disabled', - [ - 'slateUsername' => $networkUserRecord['PrimaryEmail']['Data'], - 'slateDomain' => $NetworkSchool->Domain - ] - ); - } + Debug::dump($NetworkUserMapping, true, 'mapping'); + } else { + // \Debug::dump([$networkUserRecord, $NetworkUser, $NetworkUserMapping], true, 'network user/mapping'); + } - // create a new network user if it does not exist - if (!$NetworkUser) { - $NetworkUser = User::create([ - 'FirstName' => $networkUserRecord['FirstName'], - 'LastName' => $networkUserRecord['LastName'], - 'Email' => $networkUserRecord['PrimaryEmail']['Data'], - 'StudentNumber' => $networkUserRecord['StudentNumber'], - ]); + $userChanges = static::getNetworkUserChanges($NetworkUser, $networkUserRecord, $logger); + + $SchoolUser = static::addNetworkUserToSchool($NetworkUser, $NetworkSchool, $networkUserRecord, $logger, $pretend); + + if ($userChanges->hasChanges()) { + $logger->debug( + 'Updating Network User {schoolEmail}', + [ + 'schoolEmail' => $NetworkUser->Email, + 'changes' => $userChanges + ] + ); + + $NetworkUser->setFields($userChanges->getNewValues()); + if (!$pretend) { + $NetworkUser->save(); + } + + return new SyncResult( + SyncResult::STATUS_UPDATED, + 'Updated network user {slatePrimaryEmail} @ {slateDomain}', + [ + 'slatePrimaryEmail' => $NetworkUser->Email, + 'slateDomain' => $NetworkSchool->Domain + ] + ); + } else { + return new SyncResult( + SyncResult::STATUS_VERIFIED, + 'Network user {slatePrimaryEmail} @ {slateDomain} found and verified', + [ + 'slatePrimaryEmail' => $NetworkUser->Email, + 'slateDomain' => $NetworkSchool->Domain + ] + ); + } + } else { // user mapping does not exist, let's create a user/mapping if neccessary + + if (!$NetworkUser = User::getFromRecord($networkUserRecord)) { + $NetworkUser = User::createFromRecord($networkUserRecord); + } if (!$NetworkUser->validate(true)) { $logger->error( @@ -303,21 +330,21 @@ public static function syncNetworkUser(School $NetworkSchool, array $networkUser ); } - if (!$pretend) { - $NetworkUser->save(true); - } - $logger->notice( 'Created network user for {slatePrimaryEmail}', [ 'slatePrimaryEmail' => $NetworkUser->Email - // UPDATED because we no longer extend SLATE - // $NetworkUserPrimaryEmail->Data ] ); + if (!$pretend) { + $NetworkUser->save(true); + } + // add user to school if they do not exist yet - static::addUserToSchool($NetworkUser, $NetworkSchool, $networkUserRecord); + $SchoolUser = static::addNetworkUserToSchool($NetworkUser, $NetworkSchool, $networkUserRecord, $logger, $pretend); + + $NetworkMapping = SchoolUserMapping::createByExternalIdentifier($SchoolUser, $networkUserRecord['ID'], !$pretend); return new SyncResult( SyncResult::STATUS_CREATED, @@ -327,101 +354,159 @@ public static function syncNetworkUser(School $NetworkSchool, array $networkUser 'LastName' => $NetworkUser->LastName, 'Username' => $NetworkUser->Username, 'slatePrimaryEmail' => $NetworkUser->Email, - 'slateDomain' => $NetworkSchool->Domain + 'slateDomain' => $NetworkSchool->Domain, + 'Mapping' => $NetworkMapping ] ); + } + } - } else { - $userChanges = new KeyedDiff(); + protected static function getNetworkUserChanges(User $NetworkUser, array $networkUserRecord) + { + $userChanges = new KeyedDiff(); - if ($NetworkUser->FirstName != $networkUserRecord['FirstName']) { - $userChanges->addChange('FirstName', $networkUserRecord['FirstName'], $NetworkUser->FirstName); - } + if ($NetworkUser->FirstName != $networkUserRecord['FirstName']) { + $userChanges->addChange('FirstName', $networkUserRecord['FirstName'], $NetworkUser->FirstName); + } - if ($NetworkUser->LastName != $networkUserRecord['LastName']) { - $userChanges->addChange('LastName', $networkUserRecord['LastName'], $NetworkUser->LastName); - } + if ($NetworkUser->LastName != $networkUserRecord['LastName']) { + $userChanges->addChange('LastName', $networkUserRecord['LastName'], $NetworkUser->LastName); + } - if ($NetworkUser->StudentNumber != $networkUserRecord['StudentNumber']) { - $userChanges->addChange('StudentNumber', $networkUserRecord['StudentNumber'], $NetworkUser->StudentNumber); - } + if ($NetworkUser->StudentNumber != $networkUserRecord['StudentNumber']) { + $userChanges->addChange('StudentNumber', $networkUserRecord['StudentNumber'], $NetworkUser->StudentNumber); + } - // todo: remove because we are using email as primary key - // if ($NetworkUser->Email != $networkUserRecord['PrimaryEmail']['Data']) { - // $userChanges->addChange('Email', $networkUserRecord['PrimaryEmail']['Data'], $NetworkUser->Email); - // } + // implement after mappings are implemented + // if ($NetworkUser->Email != $networkUserRecord['PrimaryEmail']['Data']) { + // $userChanges->addChange('Email', $networkUserRecord['PrimaryEmail']['Data'], $NetworkUser->Email); + // } - if ( - $userChanges->hasChanges() - ) { + return $userChanges; + } - if ($userChanges->hasChanges()) { - $NetworkUser->setFields($userChanges->getNewValues()); - $logger->debug( - 'Updating Network User {schoolEmail}', - [ - 'schoolEmail' => $NetworkUser->Email, - 'changes' => $userChanges - ] - ); - } + protected static function networkUserShouldNotBeSynced(array $networkUserRecord, School $NetworkSchool) + { + $requiredFields = [ + 'PrimaryEmail', + 'Username', + 'AccountLevel', + 'FirstName', + 'LastName', + 'ID' + ]; - static::addUserToSchool($NetworkUser, $NetworkSchool, $networkUserRecord); + $missingRequiredField = null; + foreach ($requiredFields as $requiredField) { + if (empty($networkUserRecord[$requiredField])) { + $missingRequiredField = $requiredField; + break; + } + } - if (!$pretend) { - $NetworkUser->save(true); - } + if (!empty($missingRequiredField)) { + return new SyncResult( + SyncResult::STATUS_SKIPPED, + 'Skipped record -- missing required field: {missingRequiredField}', + [ + 'missingRequiredField' => $missingRequiredField, + // 'networkUserData' => http_build_query($networkUserRecord) + ] + ); + } - return new SyncResult( - SyncResult::STATUS_UPDATED, - 'Updated network user {slatePrimaryEmail} @ {slateDomain}', - [ - 'slatePrimaryEmail' => $NetworkUser->Email, - 'slateDomain' => $NetworkSchool->Domain - ] - ); - } + if (empty($networkUserRecord['PrimaryEmail']['Data'])) { + return new SyncResult( + SyncResult::STATUS_SKIPPED, + 'Skipped {slateUsername} @ {slateDomain} due to missing PrimaryEmail', + [ + 'slateUsername' => $networkUserRecord['Username'], + 'slateDomain' => $NetworkSchool->Domain + ] + ); } + } - return new SyncResult( - SyncResult::STATUS_VERIFIED, - 'Network user {slatePrimaryEmail} @ {slateDomain} found and verified', - [ - 'slatePrimaryEmail' => $NetworkUser->Email, - 'slateDomain' => $NetworkSchool->Domain - ] - ); + protected static function getNetworkUserAccountLevel(array $networkUserData) + { + $accountTypeValues = SchoolUser::getFieldOptions('AccountType')['values']; + + $accountType = null; + if ($networkUserData['Class'] === 'Slate\\People\\Student') { + $accountType = 'Student'; + } elseif (in_array($networkUserData['AccountLevel'], $accountTypeValues)) { + $accountType = $networkUserData['AccountLevel']; + } + + return $accountType; } - protected static function addUserToSchool(IUser $User, School $School, array $networkUserData = []) + // todo: implement logger + protected static function addNetworkUserToSchool(IUser $User, School $School, array $networkUserData = [], LoggerInterface $logger = null, $pretend = true) { $conditions = [ 'SchoolID' => $School->ID, 'PersonID' => $User->ID, ]; - // set account type - if (!empty($networkUserData)) { - $accountTypeValues = SchoolUser::getFieldOptions('AccountType')['values']; - - if ($networkUserData['Class'] === 'Slate\\People\\Student') { - $accountType = 'Student'; - } elseif (in_array($networkUserData['AccountLevel'], $accountTypeValues)) { - $accountType = $networkUserData['AccountLevel']; - } - } - $SchoolUser = SchoolUser::getByWhere($conditions); + if (!$SchoolUser) { - // create school-user $SchoolUser = SchoolUser::create($conditions); } $SchoolUser->setFields([ - 'AccountType' => $accountType, + 'AccountType' => static::getNetworkUserAccountLevel($networkUserData), 'Username' => $networkUserData['Username'] ]); - $SchoolUser->save(true); + if ($SchoolUser->isPhantom || $SchoolUser->isDirty) { + if ($pretend && $SchoolUser->PersonID == 0) { // set person id in pretend-mode to pass validations + $SchoolUser->PersonID = 1; + } + + if (!$SchoolUser->validate()) { + $logger->error( + 'Invalid Record. Unable to add user {slateEmail} to school ({schoolHandle}) {slateDomain}', + [ + 'slateEmail' => $networkUserData['PrimaryEmail']['Data'], + 'slateDomain' => $School->Domain, + 'schoolHandle' => $School->Handle + ] + ); + + return $SchoolUser; + } + + if (!$SchoolUser->isPhantom) { + $logger->notice( + 'Updated SchoolUser {slateEmail} @ {slateDomain} ({changes})', + [ + 'slateEmail' => $networkUserData['PrimaryEmail']['Data'], + 'slateDomain' => $School->Domain, + 'changes' => http_build_query(array_intersect_key($SchoolUser->getData(), $SchoolUser->originalValues)) + ] + ); + } + + if (!$pretend) { + $SchoolUser->save(false); + } + + + return $SchoolUser; + } + + $logger->info( + 'Verified school user {slateEmail} @ {slateDomain}', + [ + 'accountLevel' => $networkUserData['AccountLevel'], + 'slateEmail' => $networkUserData['PrimaryEmail']['Data'], + 'slateDomain' => $School->Domain, + 'userClass' => $networkUserData['Class'] + ] + ); + + return $SchoolUser; } } diff --git a/php-classes/Slate/NetworkHub/School.php b/php-classes/Slate/NetworkHub/School.php index 06d852a..f9cd189 100644 --- a/php-classes/Slate/NetworkHub/School.php +++ b/php-classes/Slate/NetworkHub/School.php @@ -39,7 +39,7 @@ public function fetchNetworkUsers($params = []) throw new \Exception('APIKey must be configured to retrieve network users.'); } - $queryParameters = http_build_query(array_merge([ + $queryParameters = http_build_query(array_merge_recursive([ 'apiKey' => $this->APIKey, 'limit' => 0, 'include' => 'PrimaryEmail', @@ -65,4 +65,79 @@ public function fetchNetworkUsers($params = []) throw new \Exception("Error reading ($curlUrl) response: [$httpStatus] $response"); } } + + public function fetchNetworkGroups($params = []) + { + if (!$this->Domain) { + throw new \Exception('Domain must be configured to retrieve network users.'); + } + + if (!$this->APIKey) { + throw new \Exception('APIKey must be configured to retrieve network users.'); + } + + $queryParameters = http_build_query(array_merge([ + 'apiKey' => $this->APIKey, + 'limit' => 0, + 'include' => 'PrimaryEmail', + 'format' => 'json' + ], $params)); + + $curlUrl = $this->Protocol . $this->Domain . Connector::$apiEndpoints['groups'] . '?' . $queryParameters; + $ch = curl_init($curlUrl); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $results = curl_exec($ch); + $httpStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + if ($httpStatus == 200) { + $response = json_decode($results, true); + return $response['data']; + } else { + Logger::general_error('Slate Network School API Error', [ + 'exceptionClass' => static::class, + 'exceptionMessage' => $results, + 'exceptionCode' => $httpStatus + ]); + throw new \Exception("Error reading ($curlUrl) response: [$httpStatus] $response"); + } + } + + + public static $fetchDefaultGroups = ['students', 'staff']; + public function fetchNetworkGroupMembers($groups = []) + { + $groupsToInclude = !empty($groups) ? $groups : static::$fetchDefaultGroups; + $groups = $this->fetchNetworkGroups([ + 'parentGroup' => 'any' + ]); + $users = $this->fetchNetworkUsers([ + 'include' => 'groupIDs' + ]); + + // if (!is_array($groups) || !empty($_REQUEST['debug'])) { + // \Debug::dump($groups, true, 'groups'); + // } + $groupsById = []; + foreach ($groups as $group) { + if ( + in_array(strtolower($group['Handle']), $groupsToInclude) || + array_key_exists($group['ParentID'], $groupsById) + ) { + $groupsByHandle[$group['Handle']] = $group; + $groupsById[$group['ID']] = $group['Handle']; + } + } + + $filteredUsers = []; + foreach ($users as $user) { + if (!empty(array_intersect($user['groupIDs'], array_keys($groupsById)))) { + $filteredUsers[$user['ID']] = array_merge($user, [ + 'groups' => array_intersect_key($groupsById, array_flip($user['groupIDs'])) + ]); + } + } + + return $filteredUsers; + } } diff --git a/php-classes/Slate/NetworkHub/SchoolUser.php b/php-classes/Slate/NetworkHub/SchoolUser.php index 00430e0..d339731 100644 --- a/php-classes/Slate/NetworkHub/SchoolUser.php +++ b/php-classes/Slate/NetworkHub/SchoolUser.php @@ -35,10 +35,10 @@ class SchoolUser extends ActiveRecord ]; public static $validators = [ - 'SchoolID' => [ + 'School' => [ 'validator' => 'require-relationship' ], - 'PersonID' => [ + 'Person' => [ 'validator' => 'require-relationship' ] ]; @@ -93,4 +93,14 @@ public function validate($deep = true) // save results return $this->finishValidation(); } + + public function getMapping() + { + return SchoolUserMapping::getByWhere([ + 'Connector' => Connector::getConnectorId(), + 'ContextClass' => $this->getRootClass(), + 'ContextID' => $this->ID, + 'ExternalKey' => 'user[ID]' + ]); + } } diff --git a/php-classes/Slate/NetworkHub/SchoolUserMapping.php b/php-classes/Slate/NetworkHub/SchoolUserMapping.php new file mode 100644 index 0000000..c847241 --- /dev/null +++ b/php-classes/Slate/NetworkHub/SchoolUserMapping.php @@ -0,0 +1,43 @@ + $NetworkSchool->Handle, + 'ContextClass' => SchoolUser::getRootClass(), + 'ExternalKey' => 'user[ID]', + 'ExternalIdentifier' => $Id + ]; + + return static::getByWhere($conditions); + } + + public static function createByExternalIdentifier(SchoolUser $SchoolUser, $Id, $autoSave = false) + { + $conditions = [ + 'Connector' => $SchoolUser->School->Handle, + 'ContextClass' => $SchoolUser->getRootClass(), + 'ContextID' => $SchoolUser->ID, + 'ExternalKey' => 'user[ID]', + 'ExternalIdentifier' => $Id + ]; + + return static::create($conditions, $autoSave); + } + + public function getNetworkUser() + { + return $this->Context; + } +} \ No newline at end of file diff --git a/php-classes/Slate/NetworkHub/User.php b/php-classes/Slate/NetworkHub/User.php index ea91e94..50c3c52 100644 --- a/php-classes/Slate/NetworkHub/User.php +++ b/php-classes/Slate/NetworkHub/User.php @@ -5,16 +5,35 @@ class User extends \Emergence\People\User { public static $fields = [ - 'SchoolNumber' + 'StudentNumber', + 'SchoolUsername' ]; public static $relationships = [ 'Schools' => [ 'class' => School::class, - 'type' => 'one-many', + 'type' => 'many-many', 'linkClass' => SchoolUser::class, 'linkLocal' => 'PersonID', 'linkForeign' => 'SchoolID' ] ]; + + public static function getFromRecord(array $networkUserRecord) + { + return static::getByWhere([ + 'Email' => $networkUserRecord['PrimaryEmail']['Data'] + ]); + } + + public static function createFromRecord(array $networkUserRecord) + { + return static::create([ + 'FirstName' => $networkUserRecord['FirstName'], + 'LastName' => $networkUserRecord['LastName'], + 'Email' => $networkUserRecord['PrimaryEmail']['Data'], + 'StudentNumber' => $networkUserRecord['StudentNumber'], + 'Username' => static::getUniqueUsername($networkUserRecord['FirstName'], $networkUserRecord['LastName']), + ]); + } } From f4d7d0c5aaf3d38ac9bae1d2121983b974b7910d Mon Sep 17 00:00:00 2001 From: Chris Alfano Date: Sun, 26 Sep 2021 21:51:36 -0400 Subject: [PATCH 2/3] chore(deps): bup emergence-saml2 to v1.0.0 --- .holo/sources/emergence-saml2.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.holo/sources/emergence-saml2.toml b/.holo/sources/emergence-saml2.toml index 3010941..e826c2d 100644 --- a/.holo/sources/emergence-saml2.toml +++ b/.holo/sources/emergence-saml2.toml @@ -1,6 +1,6 @@ [holosource] url = "https://github.com/JarvusInnovations/emergence-saml2.git" -ref = "refs/heads/master" +ref = "refs/tags/v1.0.0" [holosource.project] holobranch = "emergence-layer" From 7a337ba87d3b33cf73ceabb39b433a24b2026f3e Mon Sep 17 00:00:00 2001 From: Chris Alfano Date: Sun, 17 Oct 2021 12:36:59 -0400 Subject: [PATCH 3/3] feat(ci): add automated release workflow --- .github/workflows/release-deploy.yml | 42 +++++++++ .github/workflows/release-prepare.yml | 113 +++++++++++++++++++++++++ .github/workflows/release-validate.yml | 33 ++++++++ 3 files changed, 188 insertions(+) create mode 100644 .github/workflows/release-deploy.yml create mode 100644 .github/workflows/release-prepare.yml create mode 100644 .github/workflows/release-validate.yml diff --git a/.github/workflows/release-deploy.yml b/.github/workflows/release-deploy.yml new file mode 100644 index 0000000..22815ec --- /dev/null +++ b/.github/workflows/release-deploy.yml @@ -0,0 +1,42 @@ +name: 'Release: Deploy PR' + +on: + pull_request: + branches: [ master ] + types: [ closed ] + +jobs: + release-deploy: + if: github.event.pull_request.merged == true # only run on PR merge + runs-on: ubuntu-latest + steps: + + - name: Configure release + run: | + PR_TITLE=$(jq -r ".pull_request.title" $GITHUB_EVENT_PATH) + PR_BODY=$(jq -r ".pull_request.body" $GITHUB_EVENT_PATH) + RELEASE_TAG=$(echo "${PR_TITLE}" | grep -oP "(?<=^Release: )v\d+\.\d+\.\d+(-rc\.\d+)?$") + + if [[ "${RELEASE_TAG}" =~ -rc\.[0-9]+$ ]]; then + RELEASE_PRERELEASE=true + else + RELEASE_PRERELEASE=false + fi + + echo "PR_TITLE=${PR_TITLE}" >> $GITHUB_ENV + echo "RELEASE_TAG=${RELEASE_TAG}" >> $GITHUB_ENV + echo "RELEASE_PRERELEASE=${RELEASE_PRERELEASE}" >> $GITHUB_ENV + + echo 'PR_BODY<> $GITHUB_ENV + echo "${PR_BODY}" >> $GITHUB_ENV + echo 'END_OF_PR_BODY' >> $GITHUB_ENV + + - name: Create release + uses: ncipollo/release-action@v1 + with: + token: ${{ secrets.BOT_GITHUB_TOKEN }} + commit: '${{ github.sha }}' + tag: '${{ env.RELEASE_TAG }}' + body: '${{ env.PR_BODY }}' + draft: false + prerelease: ${{ env.RELEASE_PRERELEASE }} diff --git a/.github/workflows/release-prepare.yml b/.github/workflows/release-prepare.yml new file mode 100644 index 0000000..303087f --- /dev/null +++ b/.github/workflows/release-prepare.yml @@ -0,0 +1,113 @@ +name: 'Release: Prepare PR' + +on: + push: + branches: [ develop ] + +env: + GITHUB_USERNAME: jarvus-bot + GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }} + RELEASE_BRANCH: master + +jobs: + release-prepare: + runs-on: ubuntu-latest + steps: + + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + # - uses: mxschmitt/action-tmate@v3 + + - name: Create/update pull request + run: | + # get latest release tag + latest_release=$(git describe --tags --abbrev=0 "origin/${RELEASE_BRANCH}") + latest_release_bumped=$(echo $latest_release | awk -F. -v OFS=. '{$NF++;print}') + + + # create or update PR + pr_body="$(cat < /tmp/pr.json + pr_number=$(hub pr list -h develop -f '%I') + echo "Opened PR #${pr_number}" + fi + + + # build changelog + commits=$( + git log \ + --first-parent \ + --reverse \ + --format="%H" \ + "origin/${RELEASE_BRANCH}..develop" + ) + + changelog=() + + while read -r commit; do + subject="$(git show -s --format=%s "${commit}")" + line="" + + if [[ "${subject}" =~ Merge\ pull\ request\ \#([0-9]+) ]]; then + line="$(hub pr show -f '%t [%i] @%au' "${BASH_REMATCH[1]}" || true)" + fi + + if [ -z "${line}" ]; then + author="$(hub api "/repos/${GITHUB_REPOSITORY}/commits/${commit}" -H Accept:application/vnd.github.v3+json | jq -r '.author.login')" + if [ -n "${author}" ]; then + author="@${author}" + else + author="$(git show -s --format=%ae "${commit}")" + fi + + line="${subject} ${author}" + fi + + # move ticket number prefix into to existing square brackets at end + line="$(echo "${line}" | perl -pe 's/^([A-Z]+-[0-9]+):?\s*(.*?)\s*\[([^]]+)\]\s*(\S+)$/\2 [\3, \1] \4/')" + + # move ticket number prefix into to new square brackets at end + line="$(echo "${line}" | perl -pe 's/^([A-Z]+-[0-9]+):?\s*(.*?)\s*(\S+)$/\2 [\1] \3/')" + + # combine doubled square brackets at the end + line="$(echo "${line}" | perl -pe 's/^\s*(.*?)\s*\[([A-Z]+-[0-9]+)\]\s*\[([^]]+)\]\s*(\S+)$/\1 [\3, \2] \4/')" + + changelog+=("- ${line}") + done <<< "${commits}" + + + # create or update comment + comment_body="$(cat <> $GITHUB_ENV + else + echo 'PR title must match format "Release: vX.Y.Z(-rc.#)?"' + exit 1 + fi + + # check that tag doesn't exist + if git ls-remote --exit-code "https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}" "refs/tags/${RELEASE_TAG}"; then + echo "The PR title's version exists already" + exit 1 + fi