Promises have three states
- Pending - this is how promises start.
- Fulfilled - this is what happens when you resolve a deferred, or when the return value from .thenfulfills, and it generally analogous to a standard return value.
- Rejected - This is what happens when you reject a deferred, when you throwfrom a.thenhandler or when you return a promise that unwraps to a rejection*, it is generally analogous to a standard exception thrown.
In Angular, promises resolve asynchronously and provide their guarantees by resolving via $rootScope.$evalAsync(callback); (taken from here). 
Since it is run via $evalAsync we know that at least one digest cycle will happen after the promise resolves (normally), since it will schedule a new digest if one is not in progress.
This is also why for example when you want to unit test promise code in Angular, you need to run a digest loop (generally, on rootScope via $rootScope.digest()) since $evalAsync execution is part of the digest loop.
Ok, enough talk, show me the code:
Note: This shows the code paths from Angular 1.2, the code paths in Angular 1.x are all similar but in 1.3+ $q has been refactored to use prototypical inheritance so this answer is not accurate in code (but is in spirit) for those versions.
1) When $q is created it does this:
  this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) {
    return qFactory(function(callback) {
      $rootScope.$evalAsync(callback);
    }, $exceptionHandler);
  }];
Which in turn, does:
function qFactory(nextTick, exceptionHandler) {
And only resolves on nextTick passed as $evalAsync inside resolve and notify:
  resolve: function(val) {
    if (pending) {
      var callbacks = pending;
      pending = undefined;
      value = ref(val);
      if (callbacks.length) {
        nextTick(function() {
          var callback;
          for (var i = 0, ii = callbacks.length; i < ii; i++) {
            callback = callbacks[i];
            value.then(callback[0], callback[1], callback[2]);
          }
        });
      }
    }
  },
On the root scope, $evalAsync is defined as:
  $evalAsync: function(expr) {
    // if we are outside of an $digest loop and this is the first time we are scheduling async
    // task also schedule async auto-flush
    if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) {
      $browser.defer(function() {
        if ($rootScope.$$asyncQueue.length) {
          $rootScope.$digest();
        }
      });
    }
    this.$$asyncQueue.push({scope: this, expression: expr});
  },
  $$postDigest : function(fn) {
    this.$$postDigestQueue.push(fn);
  },
Which, as you can see indeed schedules a digest if we are not in one and no digest has previously been scheduled. Then it pushes the function to the $$asyncQueue. 
In turn inside $digest (during a cycle, and before testing the watchers):
 asyncQueue = this.$$asyncQueue,
 ...
 while(asyncQueue.length) {
      try {
          asyncTask = asyncQueue.shift();
          asyncTask.scope.$eval(asyncTask.expression);
      } catch (e) {
          clearPhase();
          $exceptionHandler(e);
      }
      lastDirtyWatch = null;
 }
So, as we can see, it runs on the $$asyncQueue until it's empty, executing the code in your promise.
So, as we can see, updating the scope is simply assigning to it, a digest will run if it's not already running, and if it is, the code inside the promise, run on $evalAsync is called before the watchers are run. So a simple:
myPromise().then(function(result){
    $scope.someName = result;
});
Suffices, keeping it simple.
 * note angular distinguishes throws from rejections - throws are logged by default and rejections have to be logged explicitly