liyujie
2025-08-28 b3810562527858a3b3d98ffa6e9c9c5b0f4a9a8e
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
#!/usr/bin/python
 
# Copyright (c) 2016 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
 
"""Module to automate the process of deploying to production.
 
Example usage of this script:
  1. Update both autotest and chromite to the lastest commit that has passed
     the test instance.
     $ ./site_utils/automated_deploy.py
  2. Skip updating a repo, e.g. autotest
     $ ./site_utils/automated_deploy.py --skip_autotest
  3. Update a given repo to a specific commit
     $ ./site_utils/automated_deploy.py --autotest_hash='1234'
"""
 
import argparse
import os
import re
import sys
import subprocess
 
import common
from autotest_lib.client.common_lib import revision_control
from autotest_lib.site_utils.lib import infra
 
AUTOTEST_DIR = common.autotest_dir
GIT_URL = {'autotest':
           'https://chromium.googlesource.com/chromiumos/third_party/autotest',
           'chromite':
           'https://chromium.googlesource.com/chromiumos/chromite'}
PROD_BRANCH = 'prod'
MASTER_AFE = 'cautotest'
NOTIFY_GROUP = 'chromeos-infra-discuss@google.com'
 
# CIPD packages whose prod refs should be updated.
_CIPD_PACKAGES = (
        'chromiumos/infra/lucifer',
        'chromiumos/infra/skylab/linux-amd64',
        'chromiumos/infra/skylab-inventory',
        'chromiumos/infra/skylab_swarming_worker/linux-amd64',
)
 
 
class AutoDeployException(Exception):
    """Raised when any deploy step fails."""
 
 
def parse_arguments():
    """Parse command line arguments.
 
    @returns An argparse.Namespace populated with argument values.
    """
    parser = argparse.ArgumentParser(
            description=('Command to update prod branch for autotest, chromite '
                         'repos. Then deploy new changes to all lab servers.'))
    parser.add_argument('--skip_autotest', action='store_true', default=False,
            help='Skip updating autotest prod branch. Default is False.')
    parser.add_argument('--skip_chromite', action='store_true', default=False,
            help='Skip updating chromite prod branch. Default is False.')
    parser.add_argument('--force_update', action='store_true', default=False,
            help=('Force a deployment without updating both autotest and '
                  'chromite prod branch'))
    parser.add_argument('--autotest_hash', type=str, default=None,
            help='Update autotest prod branch to the given hash. If it is not'
                 ' specified, autotest prod branch will be rebased to '
                 'prod-next branch, which is the latest commit that has '
                 'passed our test instance.')
    parser.add_argument('--chromite_hash', type=str, default=None,
            help='Same as autotest_hash option.')
 
    results = parser.parse_args(sys.argv[1:])
 
    # Verify the validity of the options.
    if ((results.skip_autotest and results.autotest_hash) or
        (results.skip_chromite and results.chromite_hash)):
        parser.print_help()
        print 'Cannot specify skip_* and *_hash options at the same time.'
        sys.exit(1)
    if results.force_update:
      results.skip_autotest = True
      results.skip_chromite = True
    return results
 
 
def clone_prod_branch(repo):
    """Method to clone the prod branch for a given repo under /tmp/ dir.
 
    @param repo: Name of the git repo to be cloned.
 
    @returns path to the cloned repo.
    @raises subprocess.CalledProcessError on a command failure.
    @raised revision_control.GitCloneError when git clone fails.
    """
    repo_dir = '/tmp/%s' % repo
    print 'Cloning %s prod branch under %s' % (repo, repo_dir)
    if os.path.exists(repo_dir):
        infra.local_runner('rm -rf %s' % repo_dir)
    git_repo = revision_control.GitRepo(repo_dir, GIT_URL[repo])
    git_repo.clone(remote_branch=PROD_BRANCH)
    print 'Successfully cloned %s prod branch' % repo
    return repo_dir
 
 
def update_prod_branch(repo, repo_dir, hash_to_rebase):
    """Method to update the prod branch of the given repo to the given hash.
 
    @param repo: Name of the git repo to be updated.
    @param repo_dir: path to the cloned repo.
    @param hash_to_rebase: Hash to rebase the prod branch to. If it is None,
                           prod branch will rebase to prod-next branch.
 
    @returns the range of the pushed commits as a string. E.g 123...345. If the
        prod branch is already up-to-date, return None.
    @raises subprocess.CalledProcessError on a command failure.
    """
    with infra.chdir(repo_dir):
        print 'Updating %s prod branch.' % repo
        rebase_to = hash_to_rebase if hash_to_rebase else 'origin/prod-next'
        # Check whether prod branch is already up-to-date, which means there is
        # no changes since last push.
        print 'Detecting new changes since last push...'
        diff = infra.local_runner('git log prod..%s --oneline' % rebase_to,
                                  stream_output=True)
        if diff:
            print 'Find new changes, will update prod branch...'
            infra.local_runner('git rebase %s prod' % rebase_to,
                               stream_output=True)
            result = infra.local_runner('git push origin prod',
                                        stream_output=True)
            print 'Successfully pushed %s prod branch!\n' % repo
 
            # Get the pushed commit range, which is used to get pushed commits
            # using git log E.g. 123..456, then run git log --oneline 123..456.
            grep = re.search('(\w)*\.\.(\w)*', result)
 
            if not grep:
                raise AutoDeployException(
                    'Fail to get pushed commits for repo %s from git log: %s' %
                    (repo, result))
            return grep.group(0)
        else:
            print 'No new %s changes found since last push.' % repo
            return None
 
 
def get_pushed_commits(repo, repo_dir, pushed_commits_range):
    """Method to get the pushed commits.
 
    @param repo: Name of the updated git repo.
    @param repo_dir: path to the cloned repo.
    @param pushed_commits_range: The range of the pushed commits. E.g 123...345
    @return: the commits that are pushed to prod branch. The format likes this:
             "git log --oneline A...B | grep autotest
              A xxxx
              B xxxx"
    @raises subprocess.CalledProcessError on a command failure.
    """
    print 'Getting pushed CLs for %s repo.' % repo
    if not pushed_commits_range:
        return '\n%s:\nNo new changes since last push.' % repo
 
    with infra.chdir(repo_dir):
        get_commits_cmd = 'git log --oneline %s' % pushed_commits_range
 
        pushed_commits = infra.local_runner(
                get_commits_cmd, stream_output=True)
        if repo == 'autotest':
            autotest_commits = ''
            for cl in pushed_commits.splitlines():
                if 'autotest' in cl:
                    autotest_commits += '%s\n' % cl
 
            pushed_commits = autotest_commits
 
        print 'Successfully got pushed CLs for %s repo!\n' % repo
        displayed_cmd = get_commits_cmd
        if repo == 'autotest':
          displayed_cmd += ' | grep autotest'
        return '\n%s:\n%s\n%s\n' % (repo, displayed_cmd, pushed_commits)
 
 
def kick_off_deploy():
    """Method to kick off deploy script to deploy changes to lab servers.
 
    @raises subprocess.CalledProcessError on a repo command failure.
    """
    print 'Start deploying changes to all lab servers...'
    with infra.chdir(AUTOTEST_DIR):
        # Then kick off the deploy script.
        deploy_cmd = ('runlocalssh ./site_utils/deploy_server.py -x --afe=%s' %
                      MASTER_AFE)
        infra.local_runner(deploy_cmd, stream_output=True)
        print 'Successfully deployed changes to all lab servers.'
 
 
def main(args):
    """Main entry"""
    options = parse_arguments()
    repos = dict()
    if not options.skip_autotest:
        repos.update({'autotest': options.autotest_hash})
    if not options.skip_chromite:
        repos.update({'chromite': options.chromite_hash})
 
    print 'Moving CIPD prod refs to prod-next'
    for pkg in _CIPD_PACKAGES:
        subprocess.check_call(['cipd', 'set-ref', pkg, '-version', 'prod-next',
                               '-ref', 'prod'])
    try:
        # update_log saves the git log of the updated repo.
        update_log = ''
        for repo, hash_to_rebase in repos.iteritems():
            repo_dir = clone_prod_branch(repo)
            push_commits_range = update_prod_branch(
                repo, repo_dir, hash_to_rebase)
            update_log += get_pushed_commits(repo, repo_dir, push_commits_range)
 
        kick_off_deploy()
    except revision_control.GitCloneError as e:
        print 'Fail to clone prod branch. Error:\n%s\n' % e
        raise
    except subprocess.CalledProcessError as e:
        print ('Deploy fails when running a subprocess cmd :\n%s\n'
               'Below is the push log:\n%s\n' % (e.output, update_log))
        raise
    except Exception as e:
        print 'Deploy fails with error:\n%s\nPush log:\n%s\n' % (e, update_log)
        raise
 
    # When deploy succeeds, print the update_log.
    print ('Deploy succeeds!!! Below is the push log of the updated repo:\n%s'
           'Please email this to %s.'% (update_log, NOTIFY_GROUP))
 
 
if __name__ == '__main__':
    sys.exit(main(sys.argv))