Merge lp:~ricardokirkner/locolander/requirements into lp:locolander

Proposed by Ricardo Kirkner
Status: Merged
Approved by: Ricardo Kirkner
Approved revision: 15
Merged at revision: 10
Proposed branch: lp:~ricardokirkner/locolander/requirements
Merge into: lp:locolander
Diff against target: 507 lines (+210/-80)
6 files modified
.locolander.yml (+26/-0)
fabfile.py (+26/-0)
ns2df.py (+69/-31)
requirements.txt (+16/-2)
run_tests.sh (+1/-1)
tests/test_ns2df.py (+72/-46)
To merge this branch: bzr merge lp:~ricardokirkner/locolander/requirements
Reviewer Review Type Date Requested Status
Facundo Batista Approve
Review via email: mp+172214@code.launchpad.net

Commit message

pre-requisites to make locolander able to land itself

- updated requirements
- updated locolander config
- updated ns2df.py script to create Dockerfile from locolander config

To post a comment you must log in.
12. By Ricardo Kirkner

fixed test script path

13. By Ricardo Kirkner

improvements to ns2df.py

- allow using a requirements file
- use locolander base images
- install dependencies using a single command for improved caching
- allow ns2df.py to be called from the commandline to create a Dockerfile
- build Dockerfile so that it includes the command to run tests
- build Dockerfile so that it times out the test run

14. By Ricardo Kirkner

include ns2df tests in test script

15. By Ricardo Kirkner

added fabfile for bootstrapping and running tests

Revision history for this message
Facundo Batista (facundo) wrote :

I see you're changing back to put all apt-get installations in the same line... you told me that we should put them all in different lines because of docker image caching... did that change?

The rest looks ok!

review: Needs Information
Revision history for this message
Ricardo Kirkner (ricardokirkner-f) wrote :

I did tell you that, but I have changed my mind. The reason being that
while each new command gives us a different cached state (which was the
original reason for having it one per line), I now think that the overload
of having to invoke pip for each dependency is just too high. Also having a
single line for all dependencies should make rebuilding the image from
cache much faster than having to iterate over each intermediate state (plus
avoiding unnecessary wasted disk space by not-really reusable containers).

Hope this makes sense.

On Wed, Jul 10, 2013 at 8:15 PM, Facundo Batista <email address hidden>wrote:

> Review: Needs Information
>
> I see you're changing back to put all apt-get installations in the same
> line... you told me that we should put them all in different lines because
> of docker image caching... did that change?
>
> The rest looks ok!
> --
>
> https://code.launchpad.net/~ricardokirkner/locolander/requirements/+merge/172214
> Your team LocoLanderos is subscribed to branch lp:locolander.
>

Revision history for this message
Facundo Batista (facundo) wrote :

Good enough!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file '.locolander.yml'
--- .locolander.yml 1970-01-01 00:00:00 +0000
+++ .locolander.yml 2013-07-09 18:45:42 +0000
@@ -0,0 +1,26 @@
1precise:
2 pip:
3 distribute: 0.6.34
4 Django: 1.5.1
5 github3.py: 0.7.0
6 httplib2: 0.8.0
7 keyring: 1.5.0
8 launchpadlib: 1.10.2
9 lazr.authentication: 0.1.2
10 lazr.restfulclient: 0.13.3
11 lazr.uri: 1.0.3
12 mock: 1.0.1
13 oauth: 1.0.1
14 pep8: 1.4.5
15 pyflakes: 0.7.2
16 PyYAML: 3.10.0
17 requests: 1.2.3
18 simplejson: 3.3.0
19 South: 0.8.1
20 testresources: 0.2.7
21 wadllib: 1.3.2
22 wsgi-intercept: 0.5.1
23 zope.interface: 4.0.5
24
25metadata:
26 test_script: ./run_tests.sh
027
=== added file 'fabfile.py'
--- fabfile.py 1970-01-01 00:00:00 +0000
+++ fabfile.py 2013-07-09 18:45:42 +0000
@@ -0,0 +1,26 @@
1from fabric.api import local
2from fabric.context_managers import prefix
3
4
5def virtualenv(func):
6 def wrapped(*args):
7 with prefix('. .env/bin/activate'):
8 return func(*args)
9 return wrapped
10
11
12def bootstrap():
13 local('virtualenv -p /usr/bin/python2 .env')
14 local('.env/bin/pip install -r requirements.txt')
15
16
17@virtualenv
18def manage(*args):
19 cmd = ['python locolander/manage.py']
20 cmd.extend(args)
21 local(' '.join(cmd))
22
23
24@virtualenv
25def test():
26 local('./run_tests.sh')
027
=== modified file 'ns2df.py'
--- ns2df.py 2013-06-22 23:29:05 +0000
+++ ns2df.py 2013-07-09 18:45:42 +0000
@@ -14,7 +14,7 @@
1414
15will be converted to15will be converted to
1616
17 from ubuntu:precise17 from locolander:precise
18 run apt-get -q -y install apache2=3.3-418 run apt-get -q -y install apache2=3.3-4
19 run apt-get -q -y install bzr19 run apt-get -q -y install bzr
20 run pip install --download=/tmp/pipcache --no-install foobar20 run pip install --download=/tmp/pipcache --no-install foobar
@@ -24,14 +24,30 @@
24and also the system will use "./test" as script24and also the system will use "./test" as script
25"""25"""
2626
27import os.path
28
27import yaml29import yaml
2830
29# the conversion between our nice base names and those that are31# the conversion between our nice base names and those that are
30# needed by docker32# needed by docker
31BASE_TRANSLATIONS = {33BASE_TRANSLATIONS = {
32 'precise': 'ubuntu:precise',34 'precise': 'locolander:precise',
33}35}
3436
37
38def _get_pip_packages(items):
39 if isinstance(items, basestring):
40 return items
41 packages = []
42 for name, ver in sorted(items.items()):
43 if ver is None:
44 p = name
45 else:
46 p = "%s==%s" % (name, ver)
47 packages.append(p)
48 return ' '.join(packages)
49
50
35def _get_base(config, **params):51def _get_base(config, **params):
36 """Process the system base stuff."""52 """Process the system base stuff."""
37 # support only one base for now53 # support only one base for now
@@ -41,6 +57,13 @@
41 return ["from " + BASE_TRANSLATIONS[base]]57 return ["from " + BASE_TRANSLATIONS[base]]
4258
4359
60def _get_code(config, **params):
61 return ['run mkdir -p {target_path}'.format(**params),
62 'add . {target_path}'.format(**params),
63 'run chown -R locolander.locolander {target_path}'.format(
64 **params)]
65
66
44def _get_depends_prev(config, **params):67def _get_depends_prev(config, **params):
45 """All the dependencies before securing machine."""68 """All the dependencies before securing machine."""
46 # support only one base for now69 # support only one base for now
@@ -50,26 +73,26 @@
50 # apt73 # apt
51 items = config.get('apt', [])74 items = config.get('apt', [])
52 if items:75 if items:
76 packages = []
53 for name, ver in sorted(items.items()):77 for name, ver in sorted(items.items()):
54 if ver is None:78 if ver is None:
55 p = name79 p = name
56 else:80 else:
57 p = "%s=%s" % (name, ver)81 p = "%s=%s" % (name, ver)
58 dep = "run apt-get -q -y install " + p82 packages.append(p)
59 dependencies.append(dep)83 params['packages'] = ' '.join(packages)
84 dependencies.append('run apt-get -q -y install {packages}'.format(**params))
6085
61 # pip86 # pip
62 items = config.get('pip', [])87 items = config.get('pip', [])
63 pip_cache_dir = params["pip_cache_dir"]
64 if items:88 if items:
65 for name, ver in sorted(items.items()):89 packages = _get_pip_packages(items)
66 if ver is None:90 pip_cache_dir = params['pip_cache_dir']
67 p = name91 dependencies.append(
68 else:92 'run cd {target_path} && pip install --download={pip_cache_dir} '
69 p = "%s==%s" % (name, ver)93 '--no-install {packages}'.format(
70 dep = "run pip install --download=%s --no-install %s" % (94 pip_cache_dir=pip_cache_dir, packages=packages,
71 pip_cache_dir, p)95 target_path=params['target_path']))
72 dependencies.append(dep)
7396
74 return dependencies97 return dependencies
7598
@@ -85,33 +108,39 @@
85108
86 # pip109 # pip
87 items = config.get('pip', [])110 items = config.get('pip', [])
88 pip_cache_dir = params["pip_cache_dir"]
89 if items:111 if items:
90 for name, ver in sorted(items.items()):112 packages = _get_pip_packages(items)
91 if ver is None:113 pip_cache_dir = params['pip_cache_dir']
92 p = name114 dependencies.append(
93 else:115 'run cd {target_path} && pip install '
94 p = "%s==%s" % (name, ver)116 '--find-links=file://{pip_cache_dir} --no-index '
95 dep = "run pip install --find-links=file://%s --no-index %s" % (117 '{packages}'.format(pip_cache_dir=pip_cache_dir,
96 pip_cache_dir, p)118 packages=packages,
97 dependencies.append(dep)119 target_path=params['target_path']))
98120
99 return dependencies121 return dependencies
100122
101123
102def _get_rest(config, **params):124def _get_rest(config, **params):
103 """The final stuff."""125 """The final stuff."""
104 return []126 return ['cmd cd {target_path} && timeout 300 {test_cmd}'.format(**params)]
105127
106128
107def parse(config_text, pip_cache_dir):129def parse(config_text, pip_cache_dir, project):
108 """Convert Nessita Syntax and return the text for Docker."""130 """Convert Nessita Syntax and return the text for Docker."""
109 config = yaml.load(config_text)131 config = yaml.load(config_text)
110 metadata = config.pop("metadata")132 metadata = config.pop("metadata")
111133
134 params = {
135 'test_cmd': metadata['test_script'],
136 'target_path': os.path.join('/srv/locolander', project),
137 'pip_cache_dir': pip_cache_dir,
138 }
139
112 # the order for calling these functions is VERY important140 # the order for calling these functions is VERY important
113 functions = [141 functions = [
114 _get_base,142 _get_base,
143 _get_code,
115 _get_depends_prev,144 _get_depends_prev,
116 _get_secure,145 _get_secure,
117 _get_depends_post,146 _get_depends_post,
@@ -119,11 +148,20 @@
119 ]148 ]
120 data = []149 data = []
121 for func in functions:150 for func in functions:
122 items = func(config, pip_cache_dir=pip_cache_dir)151 items = func(config, **params)
123 data.extend(items)152 data.extend(items)
124 docker = "\n".join(data)153 docker = "\n".join(data)
125154
126 # get stuff from the metadata155 return docker
127 script = metadata["test_script"]156
128157
129 return script, docker158if __name__ == '__main__':
159 import sys
160
161 project = sys.argv[1]
162 infile, outfile = sys.argv[2:]
163
164 config = open(infile, 'r').read()
165 data = parse(config, '/var/cache/locolander', project)
166 with open(outfile, 'w') as dockerfile:
167 dockerfile.write(data + '\n')
130168
=== modified file 'requirements.txt'
--- requirements.txt 2013-06-22 19:53:22 +0000
+++ requirements.txt 2013-07-09 18:45:42 +0000
@@ -1,7 +1,21 @@
1distribute==0.6.34
1Django==1.5.12Django==1.5.1
3github3.py==0.7.0
4httplib2==0.8
5keyring==1.5
6launchpadlib==1.10.2
7lazr.authentication==0.1.2
8lazr.restfulclient==0.13.3
9lazr.uri==1.0.3
2mock==1.0.110mock==1.0.1
11oauth==1.0.1
3pep8==1.4.512pep8==1.4.5
4pyflakes==0.7.213pyflakes==0.7.2
14PyYAML==3.10
15requests==1.2.3
16simplejson==3.3.0
5South==0.8.117South==0.8.1
6github3.py==0.7.018testresources==0.2.7
7launchpadlib==1.10.219wadllib==1.3.2
20wsgi-intercept==0.5.1
21zope.interface==4.0.5
822
=== modified file 'run_tests.sh'
--- run_tests.sh 2013-06-22 20:51:04 +0000
+++ run_tests.sh 2013-07-09 18:45:42 +0000
@@ -1,7 +1,7 @@
1#! /bin/bash1#! /bin/bash
22
3set -e3set -e
4set -x
54
6python locolander/manage.py test locolanderweb5python locolander/manage.py test locolanderweb
6python -m unittest discover
7PYTHONPATH=locolander python -m unittest discover -s locolander/repos/7PYTHONPATH=locolander python -m unittest discover -s locolander/repos/
88
=== modified file 'tests/test_ns2df.py'
--- tests/test_ns2df.py 2013-06-22 23:29:05 +0000
+++ tests/test_ns2df.py 2013-07-09 18:45:42 +0000
@@ -7,96 +7,120 @@
7class DependenciesTestCase(unittest.TestCase):7class DependenciesTestCase(unittest.TestCase):
8 """Tests for the dependencies transformer."""8 """Tests for the dependencies transformer."""
99
10 def setUp(self):
11 super(DependenciesTestCase, self).setUp()
12 self.params = {
13 'pip_cache_dir': '/tmp/pipcache',
14 'target_path': '/srv/locolander/project',
15 }
16
10 def test_no_dependencies(self):17 def test_no_dependencies(self):
11 config = dict(somebase={})18 config = dict(somebase={})
12 res = ns2df._get_depends_prev(config, pip_cache_dir="/tmp/pipcache")19 res = ns2df._get_depends_prev(config, **self.params)
13 self.assertEqual(res, [])20 self.assertEqual(res, [])
14 res = ns2df._get_depends_post(config, pip_cache_dir="/tmp/pipcache")21 res = ns2df._get_depends_post(config, **self.params)
15 self.assertEqual(res, [])22 self.assertEqual(res, [])
1623
17 def test_apt_one(self):24 def test_apt_one(self):
18 config = dict(somebase={25 config = dict(somebase={
19 'apt': {'bzr': None}26 'apt': {'bzr': None}
20 })27 })
21 res = ns2df._get_depends_prev(config, pip_cache_dir="/tmp/pipcache")28 res = ns2df._get_depends_prev(config, **self.params)
22 self.assertEqual(res, ["run apt-get -q -y install bzr"])29 self.assertEqual(res, ["run apt-get -q -y install bzr"])
2330
24 def test_apt_several(self):31 def test_apt_several(self):
25 config = dict(somebase={32 config = dict(somebase={
26 'apt': {'bzr': None, 'apache2': None}33 'apt': {'bzr': None, 'apache2': None}
27 })34 })
28 res = ns2df._get_depends_prev(config, pip_cache_dir="/tmp/pipcache")35 res = ns2df._get_depends_prev(config, **self.params)
29 self.assertEqual(res, [36 self.assertEqual(res, [
30 "run apt-get -q -y install apache2",37 "run apt-get -q -y install apache2 bzr",
31 "run apt-get -q -y install bzr",
32 ])38 ])
3339
34 def test_apt_versions(self):40 def test_apt_versions(self):
35 config = dict(somebase={41 config = dict(somebase={
36 'apt': {'bzr': None, 'apache2': '3.3-4'}42 'apt': {'bzr': None, 'apache2': '3.3-4'}
37 })43 })
38 res = ns2df._get_depends_prev(config, pip_cache_dir="/tmp/pipcache")44 res = ns2df._get_depends_prev(config, **self.params)
39 self.assertEqual(res, [45 self.assertEqual(res, [
40 "run apt-get -q -y install apache2=3.3-4",46 "run apt-get -q -y install apache2=3.3-4 bzr",
41 "run apt-get -q -y install bzr",
42 ])47 ])
4348
44 def test_pip_prev_one(self):49 def test_pip_prev_one(self):
45 config = dict(somebase={50 config = dict(somebase={
46 'pip': {'foo': None}51 'pip': {'foo': None}
47 })52 })
48 res = ns2df._get_depends_prev(config, pip_cache_dir="/tmp/pipcache")53 res = ns2df._get_depends_prev(config, **self.params)
49 self.assertEqual(res, [54 self.assertEqual(res, [
50 "run pip install --download=/tmp/pipcache --no-install foo",55 "run cd /srv/locolander/project && pip install --download=/tmp/pipcache --no-install foo",
51 ])56 ])
5257
53 def test_pip_prev_several(self):58 def test_pip_prev_several(self):
54 config = dict(somebase={59 config = dict(somebase={
55 'pip': {'foo': None, 'bar': None}60 'pip': {'foo': None, 'bar': None}
56 })61 })
57 res = ns2df._get_depends_prev(config, pip_cache_dir="/tmp/pipcache")62 res = ns2df._get_depends_prev(config, **self.params)
58 self.assertEqual(res, [63 self.assertEqual(res, [
59 "run pip install --download=/tmp/pipcache --no-install bar",64 "run cd /srv/locolander/project && pip install --download=/tmp/pipcache --no-install bar foo",
60 "run pip install --download=/tmp/pipcache --no-install foo",
61 ])65 ])
6266
63 def test_pip_prev_versions(self):67 def test_pip_prev_versions(self):
64 config = dict(somebase={68 config = dict(somebase={
65 'pip': {'foo': None, 'bar': '2.1'}69 'pip': {'foo': None, 'bar': '2.1'}
66 })70 })
67 res = ns2df._get_depends_prev(config, pip_cache_dir="/tmp/pipcache")71 res = ns2df._get_depends_prev(config, **self.params)
68 self.assertEqual(res, [72 self.assertEqual(res, [
69 "run pip install --download=/tmp/pipcache --no-install bar==2.1",73 ("run cd /srv/locolander/project && pip install --download=/tmp/pipcache --no-install "
70 "run pip install --download=/tmp/pipcache --no-install foo",74 "bar==2.1 foo"),
75 ])
76
77 def test_pip_prev_requirements(self):
78 config = dict(somebase={
79 'pip': '-r requirements.txt'
80 })
81 res = ns2df._get_depends_prev(config, **self.params)
82 self.assertEqual(res, [
83 ("run cd /srv/locolander/project && pip install --download=/tmp/pipcache --no-install "
84 "-r requirements.txt"),
71 ])85 ])
7286
73 def test_pip_post_one(self):87 def test_pip_post_one(self):
74 config = dict(somebase={88 config = dict(somebase={
75 'pip': {'foo': None}89 'pip': {'foo': None}
76 })90 })
77 res = ns2df._get_depends_post(config, pip_cache_dir="/tmp/pipcache")91 res = ns2df._get_depends_post(config, **self.params)
78 self.assertEqual(res, [92 self.assertEqual(res, [
79 "run pip install --find-links=file:///tmp/pipcache --no-index foo"93 "run cd /srv/locolander/project && pip install --find-links=file:///tmp/pipcache --no-index foo"
80 ])94 ])
8195
82 def test_pip_post_several(self):96 def test_pip_post_several(self):
83 config = dict(somebase={97 config = dict(somebase={
84 'pip': {'foo': None, 'bar': None}98 'pip': {'foo': None, 'bar': None}
85 })99 })
86 res = ns2df._get_depends_post(config, pip_cache_dir="/tmp/pipcache")100 res = ns2df._get_depends_post(config, **self.params)
87 self.assertEqual(res, [101 self.assertEqual(res, [
88 "run pip install --find-links=file:///tmp/pipcache --no-index bar",102 ("run cd /srv/locolander/project && pip install --find-links=file:///tmp/pipcache --no-index "
89 "run pip install --find-links=file:///tmp/pipcache --no-index foo",103 "bar foo"),
90 ])104 ])
91105
92 def test_pip_post_versions(self):106 def test_pip_post_versions(self):
93 config = dict(somebase={107 config = dict(somebase={
94 'pip': {'foo': None, 'bar': '2.1'}108 'pip': {'foo': None, 'bar': '2.1'}
95 })109 })
96 res = ns2df._get_depends_post(config, pip_cache_dir="/tmcache")110 res = ns2df._get_depends_post(config, **self.params)
97 self.assertEqual(res, [111 self.assertEqual(res, [
98 "run pip install --find-links=file:///tmcache --no-index bar==2.1",112 ("run cd /srv/locolander/project && pip install --find-links=file:///tmp/pipcache --no-index "
99 "run pip install --find-links=file:///tmcache --no-index foo",113 "bar==2.1 foo"),
114 ])
115
116 def test_pip_post_requirements(self):
117 config = dict(somebase={
118 'pip': '-r requirements.txt'
119 })
120 res = ns2df._get_depends_post(config, **self.params)
121 self.assertEqual(res, [
122 ("run cd /srv/locolander/project && pip install --find-links=file:///tmp/pipcache --no-index "
123 "-r requirements.txt"),
100 ])124 ])
101125
102 def test_mixed_prev(self):126 def test_mixed_prev(self):
@@ -104,12 +128,10 @@
104 'apt': {'bzr': None, 'apache2': '3.3-4'},128 'apt': {'bzr': None, 'apache2': '3.3-4'},
105 'pip': {'foo': None, 'bar': None},129 'pip': {'foo': None, 'bar': None},
106 })130 })
107 res = ns2df._get_depends_prev(config, pip_cache_dir="/tmp/pipcache")131 res = ns2df._get_depends_prev(config, **self.params)
108 self.assertEqual(res, [132 self.assertEqual(res, [
109 "run apt-get -q -y install apache2=3.3-4",133 "run apt-get -q -y install apache2=3.3-4 bzr",
110 "run apt-get -q -y install bzr",134 "run cd /srv/locolander/project && pip install --download=/tmp/pipcache --no-install bar foo",
111 "run pip install --download=/tmp/pipcache --no-install bar",
112 "run pip install --download=/tmp/pipcache --no-install foo",
113 ])135 ])
114136
115 def test_mixed_post(self):137 def test_mixed_post(self):
@@ -117,10 +139,10 @@
117 'apt': {'bzr': None, 'apache2': '3.3-4'},139 'apt': {'bzr': None, 'apache2': '3.3-4'},
118 'pip': {'foo': None, 'bar': None},140 'pip': {'foo': None, 'bar': None},
119 })141 })
120 res = ns2df._get_depends_post(config, pip_cache_dir="/tmp/pipcache")142 res = ns2df._get_depends_post(config, **self.params)
121 self.assertEqual(res, [143 self.assertEqual(res, [
122 "run pip install --find-links=file:///tmp/pipcache --no-index bar",144 ("run cd /srv/locolander/project && pip install --find-links=file:///tmp/pipcache --no-index "
123 "run pip install --find-links=file:///tmp/pipcache --no-index foo",145 "bar foo"),
124 ])146 ])
125147
126148
@@ -130,7 +152,7 @@
130 def test_one_base(self):152 def test_one_base(self):
131 config = dict(precise={})153 config = dict(precise={})
132 res = ns2df._get_base(config)154 res = ns2df._get_base(config)
133 self.assertEqual(res, ["from ubuntu:precise"])155 self.assertEqual(res, ["from locolander:precise"])
134156
135 def test_several_bases(self):157 def test_several_bases(self):
136 config = dict(base1={}, base2={})158 config = dict(base1={}, base2={})
@@ -141,8 +163,10 @@
141 self.assertEqual(res, ["run ip link set dev eth0 down"])163 self.assertEqual(res, ["run ip link set dev eth0 down"])
142164
143 def test_rest(self):165 def test_rest(self):
144 res = ns2df._get_rest({})166 res = ns2df._get_rest({},
145 self.assertEqual(res, [])167 target_path='/srv/locolander',
168 test_cmd='./run_tests.sh')
169 self.assertEqual(res, ["cmd cd /srv/locolander && timeout 300 ./run_tests.sh"])
146170
147 def test_all_mixed(self):171 def test_all_mixed(self):
148 config_text = """172 config_text = """
@@ -155,14 +179,16 @@
155 metadata:179 metadata:
156 test_script: foo180 test_script: foo
157 """181 """
158 script, docker = ns2df.parse(config_text, "/tmp/pipcache")182 docker = ns2df.parse(config_text, "/tmp/pipcache", 'project')
159 self.assertEqual(script, "foo")
160 self.assertEqual(docker,183 self.assertEqual(docker,
161 "from ubuntu:precise\n"184 "from locolander:precise\n"
162 "run apt-get -q -y install apache2=3.3-4\n"185 "run mkdir -p /srv/locolander/project\n"
163 "run apt-get -q -y install bzr\n"186 "add . /srv/locolander/project\n"
164 "run pip install --download=/tmp/pipcache --no-install foobar\n"187 "run chown -R locolander.locolander /srv/locolander/project\n"
188 "run apt-get -q -y install apache2=3.3-4 bzr\n"
189 "run cd /srv/locolander/project && pip install --download=/tmp/pipcache --no-install foobar\n"
165 "run ip link set dev eth0 down\n"190 "run ip link set dev eth0 down\n"
166 "run pip install --find-links=file:///tmp/pipcache "191 "run cd /srv/locolander/project && pip install --find-links=file:///tmp/pipcache "
167 "--no-index foobar"192 "--no-index foobar\n"
193 "cmd cd /srv/locolander/project && timeout 300 foo"
168 )194 )

Subscribers

People subscribed via source and target branches

to all changes: