Bug 1337360: enforce use of dashes in YAML schemas; r?Callek draft
authorDustin J. Mitchell <dustin@mozilla.com>
Tue, 07 Feb 2017 08:55:56 -0500
changeset 480047 cde55c1713a32ee00e843a63215aeafa0b75f9d0
parent 479115 42d594b85056e4b43ba543e3aef2a426d59131d4
child 544857 99014f398f20d49f52968db871a0e95ce7d93513
push id44438
push userdmitchell@mozilla.com
push dateTue, 07 Feb 2017 19:09:26 +0000
reviewersCallek
bugs1337360
milestone54.0a1
Bug 1337360: enforce use of dashes in YAML schemas; r?Callek MozReview-Commit-ID: BAXfUNMBg2V
taskcluster/taskgraph/test/test_util_schema.py
taskcluster/taskgraph/transforms/balrog.py
taskcluster/taskgraph/transforms/beetmover.py
taskcluster/taskgraph/transforms/build_signing.py
taskcluster/taskgraph/transforms/job/__init__.py
taskcluster/taskgraph/transforms/job/hazard.py
taskcluster/taskgraph/transforms/job/mach.py
taskcluster/taskgraph/transforms/job/mozharness.py
taskcluster/taskgraph/transforms/job/run_task.py
taskcluster/taskgraph/transforms/job/spidermonkey.py
taskcluster/taskgraph/transforms/job/toolchain.py
taskcluster/taskgraph/transforms/l10n.py
taskcluster/taskgraph/transforms/nightly_l10n_signing.py
taskcluster/taskgraph/transforms/signing.py
taskcluster/taskgraph/transforms/task.py
taskcluster/taskgraph/transforms/tests.py
taskcluster/taskgraph/util/schema.py
--- a/taskcluster/taskgraph/test/test_util_schema.py
+++ b/taskcluster/taskgraph/test/test_util_schema.py
@@ -4,18 +4,19 @@
 
 from __future__ import absolute_import, print_function, unicode_literals
 
 import unittest
 from mozunit import main
 from taskgraph.util.schema import (
     validate_schema,
     resolve_keyed_by,
+    check_schema,
 )
-from voluptuous import Schema
+from voluptuous import Schema, Optional, Any
 
 schema = Schema({
     'x': int,
     'y': basestring,
 })
 
 
 class TestValidateSchema(unittest.TestCase):
@@ -68,17 +69,18 @@ class TestResolveKeyedBy(unittest.TestCa
             resolve_keyed_by(
                 {'f': 'shoes', 'x': {'y': {'by-f': {'shoes': 'feet', 'gloves': 'hands'}}}},
                 'x.y', 'n'),
             {'f': 'shoes', 'x': {'y': 'feet'}})
 
     def test_match_regexp(self):
         self.assertEqual(
             resolve_keyed_by(
-                {'f': 'shoes', 'x': {'by-f': {'s?[hH]oes?': 'feet', 'gloves': 'hands'}}},
+                {'f': 'shoes', 'x': {
+                    'by-f': {'s?[hH]oes?': 'feet', 'gloves': 'hands'}}},
                 'x', 'n'),
             {'f': 'shoes', 'x': 'feet'})
 
     def test_match_partial_regexp(self):
         self.assertEqual(
             resolve_keyed_by(
                 {'f': 'shoes', 'x': {'by-f': {'sh': 'feet', 'default': 'hands'}}},
                 'x', 'n'),
@@ -105,10 +107,37 @@ class TestResolveKeyedBy(unittest.TestCa
             {'f': 'shoes', 'x': {'by-f': {'hat': 'head'}}}, 'x', 'n')
 
     def test_multiple_matches(self):
         self.assertRaises(
             Exception, resolve_keyed_by,
             {'f': 'hats', 'x': {'by-f': {'hat.*': 'head', 'ha.*': 'hair'}}}, 'x', 'n')
 
 
+class TestCheckSchema(unittest.TestCase):
+
+    def test_intercaps_key(self):
+        self.assertRaises(RuntimeError, lambda:
+                          check_schema(Schema({'interCaps': int})))
+
+    def test_underscore_key(self):
+        self.assertRaises(RuntimeError, lambda:
+                          check_schema(Schema({'under_scores': int})))
+
+    def test_optional_bad_key(self):
+        self.assertRaises(RuntimeError, lambda:
+                          check_schema(Schema({Optional('under_scores'): int})))
+
+    def test_nested_bad_key(self):
+        self.assertRaises(RuntimeError, lambda:
+                          check_schema(Schema({'x': {'under_scores': int}})))
+
+    def test_list_bad_key(self):
+        self.assertRaises(RuntimeError, lambda:
+                          check_schema(Schema([{'under_scores': int}])))
+
+    def test_any_bad_key(self):
+        self.assertRaises(RuntimeError, lambda:
+                          check_schema(Schema(Any({'under_scores': int}, int))))
+
+
 if __name__ == '__main__':
     main()
--- a/taskcluster/taskgraph/transforms/balrog.py
+++ b/taskcluster/taskgraph/transforms/balrog.py
@@ -3,19 +3,19 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 """
 Transform the beetmover task into an actual task description.
 """
 
 from __future__ import absolute_import, print_function, unicode_literals
 
 from taskgraph.transforms.base import TransformSequence
-from taskgraph.util.schema import validate_schema
+from taskgraph.util.schema import validate_schema, Schema
 from taskgraph.transforms.task import task_description_schema
-from voluptuous import Schema, Any, Required, Optional
+from voluptuous import Any, Required, Optional
 
 
 # Voluptuous uses marker objects as dictionary *keys*, but they are not
 # comparable, so we cast all of the keys back to regular strings
 task_description_schema = {str(k): v for k, v in task_description_schema.schema.iteritems()}
 
 transforms = TransformSequence()
 
@@ -78,18 +78,18 @@ def make_task_description(config, jobs):
 
         if dep_job.attributes.get('locale'):
             treeherder['symbol'] = 'tc-Up({})'.format(dep_job.attributes.get('locale'))
             attributes['locale'] = dep_job.attributes.get('locale')
 
         label = job.get('label', "balrog-{}".format(dep_job.label))
 
         upstream_artifacts = [{
-            "taskId": {"task-reference": "<beetmover>"},
-            "taskType": "beetmover",
+            "task-id": {"task-reference": "<beetmover>"},
+            "task-type": "beetmover",
             "paths": [
                 "public/manifest.json"
             ],
         }]
 
         task = {
             'label': label,
             'description': "{} Balrog".format(
--- a/taskcluster/taskgraph/transforms/beetmover.py
+++ b/taskcluster/taskgraph/transforms/beetmover.py
@@ -3,19 +3,19 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 """
 Transform the beetmover task into an actual task description.
 """
 
 from __future__ import absolute_import, print_function, unicode_literals
 
 from taskgraph.transforms.base import TransformSequence
-from taskgraph.util.schema import validate_schema
+from taskgraph.util.schema import validate_schema, Schema
 from taskgraph.transforms.task import task_description_schema
-from voluptuous import Schema, Any, Required, Optional
+from voluptuous import Any, Required, Optional
 
 
 _DESKTOP_UPSTREAM_ARTIFACTS_UNSIGNED_EN_US = [
     "balrog_props.json",
     "target.common.tests.zip",
     "target.cppunittest.tests.zip",
     "target.crashreporter-symbols.zip",
     "target.json",
@@ -228,27 +228,27 @@ def generate_upstream_artifacts(taskid_t
         mapping = UPSTREAM_ARTIFACT_SIGNED_PATHS
 
     artifact_prefix = 'public/build'
     if locale:
         artifact_prefix = 'public/build/{}'.format(locale)
         platform = "{}-l10n".format(platform)
 
     upstream_artifacts = [{
-        "taskId": {"task-reference": taskid_to_beetmove},
-        "taskType": task_type,
+        "task-id": {"task-reference": taskid_to_beetmove},
+        "task-type": task_type,
         "paths": ["{}/{}".format(artifact_prefix, p) for p in mapping[platform]],
         "locale": locale or "en-US",
     }]
     if not locale and "android" in platform:
         # edge case to support 'multi' locale paths
         multi_platform = "{}-multi".format(platform)
         upstream_artifacts.append({
-            "taskId": {"task-reference": taskid_to_beetmove},
-            "taskType": task_type,
+            "task-id": {"task-reference": taskid_to_beetmove},
+            "task-type": task_type,
             "paths": ["{}/{}".format(artifact_prefix, p) for p in mapping[multi_platform]],
             "locale": "multi",
         })
 
     return upstream_artifacts
 
 
 def generate_signing_upstream_artifacts(taskid_to_beetmove, taskid_of_manifest, platform,
@@ -256,18 +256,18 @@ def generate_signing_upstream_artifacts(
     upstream_artifacts = generate_upstream_artifacts(taskid_to_beetmove, platform, locale,
                                                      signing=True)
     if locale:
         artifact_prefix = 'public/build/{}'.format(locale)
     else:
         artifact_prefix = 'public/build'
     manifest_path = "{}/balrog_props.json".format(artifact_prefix)
     upstream_artifacts.append({
-        "taskId": {"task-reference": taskid_of_manifest},
-        "taskType": "build",
+        "task-id": {"task-reference": taskid_of_manifest},
+        "task-type": "build",
         "paths": [manifest_path],
         "locale": locale or "en-US",
     })
 
     return upstream_artifacts
 
 
 def generate_build_upstream_artifacts(taskid_to_beetmove, platform, locale=None):
@@ -308,17 +308,17 @@ def make_task_worker(config, jobs):
         else:
             taskid_to_beetmove = "<" + str(build_kind) + ">"
             update_manifest = False
             upstream_artifacts = generate_build_upstream_artifacts(
                 taskid_to_beetmove, platform, locale
             )
 
         worker = {'implementation': 'beetmover',
-                  'update_manifest': update_manifest,
+                  'update-manifest': update_manifest,
                   'upstream-artifacts': upstream_artifacts}
 
         if locale:
             worker["locale"] = locale
 
         job["worker"] = worker
 
         yield job
--- a/taskcluster/taskgraph/transforms/build_signing.py
+++ b/taskcluster/taskgraph/transforms/build_signing.py
@@ -35,18 +35,18 @@ def make_signing_description(config, job
                     'artifacts': ['public/build/update/target.complete.mar'],
                     'format': 'mar',
                 }
             ]
         upstream_artifacts = []
         for spec in job_specs:
             fmt = spec["format"]
             upstream_artifacts.append({
-                "taskId": {"task-reference": "<build>"},
-                "taskType": "build",
+                "task-id": {"task-reference": "<build>"},
+                "task-type": "build",
                 "paths": spec["artifacts"],
                 "formats": [fmt]
             })
 
         job['upstream-artifacts'] = upstream_artifacts
 
         label = dep_job.label.replace("build-", "signing-")
         job['label'] = label
--- a/taskcluster/taskgraph/transforms/job/__init__.py
+++ b/taskcluster/taskgraph/transforms/job/__init__.py
@@ -14,24 +14,24 @@ from __future__ import absolute_import, 
 import copy
 import logging
 import os
 
 from taskgraph.transforms.base import TransformSequence
 from taskgraph.util.schema import (
     validate_schema,
     resolve_keyed_by,
+    Schema,
 )
 from taskgraph.transforms.task import task_description_schema
 from voluptuous import (
     Any,
     Extra,
     Optional,
     Required,
-    Schema,
 )
 
 logger = logging.getLogger(__name__)
 
 # Voluptuous uses marker objects as dictionary *keys*, but they are not
 # comparable, so we cast all of the keys back to regular strings
 task_description_schema = {str(k): v for k, v in task_description_schema.schema.iteritems()}
 
--- a/taskcluster/taskgraph/transforms/job/hazard.py
+++ b/taskcluster/taskgraph/transforms/job/hazard.py
@@ -2,17 +2,18 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 """
 Support for running hazard jobs via dedicated scripts
 """
 
 from __future__ import absolute_import, print_function, unicode_literals
 
-from voluptuous import Schema, Required, Optional, Any
+from taskgraph.util.schema import Schema
+from voluptuous import Required, Optional, Any
 
 from taskgraph.transforms.job import run_job_using
 from taskgraph.transforms.job.common import (
     docker_worker_add_workspace_cache,
     docker_worker_setup_secrets,
     docker_worker_add_public_artifacts,
     docker_worker_support_vcs_checkout,
 )
--- a/taskcluster/taskgraph/transforms/job/mach.py
+++ b/taskcluster/taskgraph/transforms/job/mach.py
@@ -4,17 +4,18 @@
 """
 Support for running mach tasks (via run-task)
 """
 
 from __future__ import absolute_import, print_function, unicode_literals
 
 from taskgraph.transforms.job import run_job_using
 from taskgraph.transforms.job.run_task import docker_worker_run_task
-from voluptuous import Schema, Required
+from taskgraph.util.schema import Schema
+from voluptuous import Required
 
 mach_schema = Schema({
     Required('using'): 'mach',
 
     # The mach command (omitting `./mach`) to run
     Required('mach'): basestring,
 })
 
--- a/taskcluster/taskgraph/transforms/job/mozharness.py
+++ b/taskcluster/taskgraph/transforms/job/mozharness.py
@@ -5,17 +5,18 @@
 
 Support for running jobs via mozharness.  Ideally, most stuff gets run this
 way, and certainly anything using mozharness should use this approach.
 
 """
 
 from __future__ import absolute_import, print_function, unicode_literals
 
-from voluptuous import Schema, Required, Optional, Any
+from taskgraph.util.schema import Schema
+from voluptuous import Required, Optional, Any
 
 from taskgraph.transforms.job import run_job_using
 from taskgraph.transforms.job.common import (
     docker_worker_add_workspace_cache,
     docker_worker_add_gecko_vcs_env_vars,
     docker_worker_setup_secrets,
     docker_worker_add_public_artifacts,
     docker_worker_support_vcs_checkout,
--- a/taskcluster/taskgraph/transforms/job/run_task.py
+++ b/taskcluster/taskgraph/transforms/job/run_task.py
@@ -5,20 +5,21 @@
 Support for running jobs that are invoked via the `run-task` script.
 """
 
 from __future__ import absolute_import, print_function, unicode_literals
 
 import copy
 
 from taskgraph.transforms.job import run_job_using
+from taskgraph.util.schema import Schema
 from taskgraph.transforms.job.common import (
     docker_worker_support_vcs_checkout,
 )
-from voluptuous import Schema, Required, Any
+from voluptuous import Required, Any
 
 run_task_schema = Schema({
     Required('using'): 'run-task',
 
     # if true, add a cache at ~worker/.cache, which is where things like pip
     # tend to hide their caches.  This cache is never added for level-1 jobs.
     Required('cache-dotcache', default=False): bool,
 
--- a/taskcluster/taskgraph/transforms/job/spidermonkey.py
+++ b/taskcluster/taskgraph/transforms/job/spidermonkey.py
@@ -2,17 +2,18 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 """
 Support for running spidermonkey jobs via dedicated scripts
 """
 
 from __future__ import absolute_import, print_function, unicode_literals
 
-from voluptuous import Schema, Required, Optional, Any
+from taskgraph.util.schema import Schema
+from voluptuous import Required, Optional, Any
 
 from taskgraph.transforms.job import run_job_using
 from taskgraph.transforms.job.common import (
     docker_worker_add_public_artifacts,
     docker_worker_support_vcs_checkout,
 )
 
 sm_run_schema = Schema({
--- a/taskcluster/taskgraph/transforms/job/toolchain.py
+++ b/taskcluster/taskgraph/transforms/job/toolchain.py
@@ -2,17 +2,18 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 """
 Support for running toolchain-building jobs via dedicated scripts
 """
 
 from __future__ import absolute_import, print_function, unicode_literals
 
-from voluptuous import Schema, Required, Any
+from taskgraph.util.schema import Schema
+from voluptuous import Required, Any
 
 from taskgraph.transforms.job import run_job_using
 from taskgraph.transforms.job.common import (
     docker_worker_add_tc_vcs_cache,
     docker_worker_add_gecko_vcs_env_vars
 )
 
 toolchain_run_schema = Schema({
--- a/taskcluster/taskgraph/transforms/l10n.py
+++ b/taskcluster/taskgraph/transforms/l10n.py
@@ -12,24 +12,24 @@ import copy
 from mozbuild.chunkify import chunkify
 from taskgraph.transforms.base import (
     TransformSequence,
 )
 from taskgraph.util.schema import (
     validate_schema,
     optionally_keyed_by,
     resolve_keyed_by,
+    Schema,
 )
 from taskgraph.util.treeherder import split_symbol, join_symbol
 from voluptuous import (
     Any,
     Extra,
     Optional,
     Required,
-    Schema,
 )
 
 
 def _by_platform(arg):
     return optionally_keyed_by('build-platform', arg)
 
 # shortcut for a string where task references are allowed
 taskref_or_string = Any(
@@ -42,17 +42,17 @@ l10n_description_schema = Schema({
 
     # build-platform, inferred from dependent job before validation
     Required('build-platform'): basestring,
 
     # max run time of the task
     Required('run-time'): _by_platform(int),
 
     # Data used by chain of trust (see `chain_of_trust` in this file)
-    Optional('chainOfTrust'): {Extra: object},
+    Optional('chain-of-trust'): {Extra: object},
 
     # All l10n jobs use mozharness
     Required('mozharness'): {
         # Script to invoke for mozharness
         Required('script'): _by_platform(basestring),
 
         # Config files passed to the mozharness script
         Required('config'): _by_platform([basestring]),
@@ -95,27 +95,17 @@ l10n_description_schema = Schema({
         Required('platform'): _by_platform(basestring),
 
         # Symbol to use
         Required('symbol'): basestring,
 
         # Tier this task is
         Required('tier'): _by_platform(int),
     },
-    Required('attributes'): {
-        # Is this a nightly task, inferred from dependent job before validation
-        Optional('nightly'): bool,
-
-        # build_platform of this task, inferred from dependent job before validation
-        Required('build_platform'): basestring,
-
-        # build_type for this task, inferred from dependent job before validation
-        Required('build_type'): basestring,
-        Extra: object,
-    },
+    Required('attributes'): dict,
 
     # Extra environment values to pass to the worker
     Optional('env'): _by_platform({basestring: taskref_or_string}),
 
     # Number of chunks to split the locale repacks up into
     Optional('chunks'): _by_platform(int),
 
     # Task deps to chain this task with, added in transforms from dependent-task
@@ -303,19 +293,19 @@ def mh_options_replace_project(config, j
             job['mozharness']['options']
             )
         yield job
 
 
 @transforms.add
 def chain_of_trust(config, jobs):
     for job in jobs:
-        job.setdefault('chainOfTrust', {})
-        job['chainOfTrust'].setdefault('inputs', {})
-        job['chainOfTrust']['inputs']['docker-image'] = {
+        job.setdefault('chain-of-trust', {})
+        job['chain-of-trust'].setdefault('inputs', {})
+        job['chain-of-trust']['inputs']['docker-image'] = {
             "task-reference": "<docker-image>"
         }
         yield job
 
 
 @transforms.add
 def validate_again(config, jobs):
     for job in jobs:
@@ -330,17 +320,17 @@ def make_job_description(config, jobs):
             'name': job['name'],
             'worker': {
                 'implementation': 'docker-worker',
                 'docker-image': {'in-tree': 'desktop-build'},
                 'max-run-time': job['run-time'],
                 'chain-of-trust': True,
             },
             'extra': {
-                'chainOfTrust': job['chainOfTrust'],
+                'chainOfTrust': job['chain-of-trust'],
             },
             'worker-type': job['worker-type'],
             'description': job['description'],
             'run': {
                 'using': 'mozharness',
                 'job-script': 'taskcluster/scripts/builder/build-l10n.sh',
                 'config': job['mozharness']['config'],
                 'script': job['mozharness']['script'],
--- a/taskcluster/taskgraph/transforms/nightly_l10n_signing.py
+++ b/taskcluster/taskgraph/transforms/nightly_l10n_signing.py
@@ -39,18 +39,18 @@ def make_signing_description(config, job
                     'artifacts': ['public/build/{locale}/target.complete.mar'],
                     'format': 'mar',
                 }
             ]
         upstream_artifacts = []
         for spec in job_specs:
             fmt = spec['format']
             upstream_artifacts.append({
-                "taskId": {"task-reference": "<unsigned-repack>"},
-                "taskType": "l10n",
+                "task-id": {"task-reference": "<unsigned-repack>"},
+                "task-type": "l10n",
                 # Set paths based on artifacts in the specs (above) one per
                 # locale present in the chunk this is signing stuff for.
                 "paths": [f.format(locale=l)
                           for l in dep_job.attributes.get('chunk_locales', [])
                           for f in spec['artifacts']],
                 "formats": [fmt]
             })
 
--- a/taskcluster/taskgraph/transforms/signing.py
+++ b/taskcluster/taskgraph/transforms/signing.py
@@ -3,19 +3,19 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 """
 Transform the signing task into an actual task description.
 """
 
 from __future__ import absolute_import, print_function, unicode_literals
 
 from taskgraph.transforms.base import TransformSequence
-from taskgraph.util.schema import validate_schema
+from taskgraph.util.schema import validate_schema, Schema
 from taskgraph.transforms.task import task_description_schema
-from voluptuous import Schema, Any, Required, Optional
+from voluptuous import Any, Required, Optional
 
 
 ARTIFACT_URL = 'https://queue.taskcluster.net/v1/task/<{}>/artifacts/{}'
 
 
 # Voluptuous uses marker objects as dictionary *keys*, but they are not
 # comparable, so we cast all of the keys back to regular strings
 task_description_schema = {str(k): v for k, v in task_description_schema.schema.iteritems()}
@@ -30,20 +30,20 @@ taskref_or_string = Any(
 signing_description_schema = Schema({
     # the dependant task (object) for this signing job, used to inform signing.
     Required('dependent-task'): object,
 
     # Artifacts from dep task to sign - Sync with taskgraph/transforms/task.py
     # because this is passed directly into the signingscript worker
     Required('upstream-artifacts'): [{
         # taskId of the task with the artifact
-        Required('taskId'): taskref_or_string,
+        Required('task-id'): taskref_or_string,
 
         # type of signing task (for CoT)
-        Required('taskType'): basestring,
+        Required('task-type'): basestring,
 
         # Paths to the artifacts to sign
         Required('paths'): [basestring],
 
         # Signing formats to use on each of the paths
         Required('formats'): [basestring],
     }],
 
--- a/taskcluster/taskgraph/transforms/task.py
+++ b/taskcluster/taskgraph/transforms/task.py
@@ -10,18 +10,18 @@ complexities of worker implementations, 
 
 from __future__ import absolute_import, print_function, unicode_literals
 
 import json
 import time
 
 from taskgraph.util.treeherder import split_symbol
 from taskgraph.transforms.base import TransformSequence
-from taskgraph.util.schema import validate_schema
-from voluptuous import Schema, Any, Required, Optional, Extra
+from taskgraph.util.schema import validate_schema, Schema
+from voluptuous import Any, Required, Optional, Extra
 
 from .gecko_v2_whitelist import JOB_NAME_WHITELIST, JOB_NAME_WHITELIST_ERROR
 
 
 # shortcut for a string where task references are allowed
 taskref_or_string = Any(
     basestring,
     {Required('task-reference'): basestring})
@@ -274,64 +274,64 @@ task_description_schema = Schema({
         Required('implementation'): 'scriptworker-signing',
 
         # the maximum time to spend signing, in seconds
         Required('max-run-time', default=600): int,
 
         # list of artifact URLs for the artifacts that should be signed
         Required('upstream-artifacts'): [{
             # taskId of the task with the artifact
-            Required('taskId'): taskref_or_string,
+            Required('task-id'): taskref_or_string,
 
             # type of signing task (for CoT)
-            Required('taskType'): basestring,
+            Required('task-type'): basestring,
 
             # Paths to the artifacts to sign
             Required('paths'): [basestring],
 
             # Signing formats to use on each of the paths
             Required('formats'): [basestring],
         }],
     }, {
         Required('implementation'): 'beetmover',
 
         # the maximum time to spend signing, in seconds
         Required('max-run-time', default=600): int,
 
         # taskid of task with artifacts to beetmove
         # beetmover template key
-        Required('update_manifest'): bool,
+        Required('update-manifest'): bool,
 
         # locale key, if this is a locale beetmover job
         Optional('locale'): basestring,
 
         # list of artifact URLs for the artifacts that should be beetmoved
         Required('upstream-artifacts'): [{
             # taskId of the task with the artifact
-            Required('taskId'): taskref_or_string,
+            Required('task-id'): taskref_or_string,
 
             # type of signing task (for CoT)
-            Required('taskType'): basestring,
+            Required('task-type'): basestring,
 
             # Paths to the artifacts to sign
             Required('paths'): [basestring],
 
             # locale is used to map upload path and allow for duplicate simple names
             Required('locale'): basestring,
         }],
     }, {
         Required('implementation'): 'balrog',
 
         # list of artifact URLs for the artifacts that should be beetmoved
         Required('upstream-artifacts'): [{
             # taskId of the task with the artifact
-            Required('taskId'): taskref_or_string,
+            Required('task-id'): taskref_or_string,
 
             # type of signing task (for CoT)
-            Required('taskType'): basestring,
+            Required('task-type'): basestring,
 
             # Paths to the artifacts to sign
             Required('paths'): [basestring],
         }],
     }),
 
     # The "when" section contains descriptions of the circumstances
     # under which this task can be "optimized", that is, left out of the
@@ -542,46 +542,57 @@ def build_generic_worker_payload(config,
     }
 
     # needs-sccache is handled in mozharness_on_windows
 
     if 'retry-exit-status' in worker:
         raise Exception("retry-exit-status not supported in generic-worker")
 
 
+def format_upstream_artifacts(upstream_artifacts):
+    """Change the case of the artifact definitions to match that expected by
+    scriptworker"""
+    return [{
+        'taskId': ua['task-id'],
+        'taskType': ua['task-type'],
+        'paths': ua['paths'],
+        'formats': ua['formats'],
+    } for ua in upstream_artifacts]
+
+
 @payload_builder('scriptworker-signing')
 def build_scriptworker_signing_payload(config, task, task_def):
     worker = task['worker']
 
     task_def['payload'] = {
         'maxRunTime': worker['max-run-time'],
-        'upstreamArtifacts':  worker['upstream-artifacts']
+        'upstreamArtifacts':  format_upstream_artifacts(worker['upstream-artifacts'])
     }
 
 
 @payload_builder('beetmover')
 def build_beetmover_payload(config, task, task_def):
     worker = task['worker']
 
     task_def['payload'] = {
         'maxRunTime': worker['max-run-time'],
         'upload_date': config.params['build_date'],
-        'update_manifest': worker['update_manifest'],
-        'upstreamArtifacts':  worker['upstream-artifacts']
+        'update_manifest': worker['update-manifest'],
+        'upstreamArtifacts':  format_upstream_artifacts(worker['upstream-artifacts'])
     }
     if worker.get('locale'):
         task_def['payload']['locale'] = worker['locale']
 
 
 @payload_builder('balrog')
 def build_balrog_payload(config, task, task_def):
     worker = task['worker']
 
     task_def['payload'] = {
-        'upstreamArtifacts':  worker['upstream-artifacts']
+        'upstreamArtifacts':  format_upstream_artifacts(worker['upstream-artifacts'])
     }
 
 
 @payload_builder('native-engine')
 def build_macosx_engine_payload(config, task, task_def):
     worker = task['worker']
     artifacts = map(lambda artifact: {
         'name': artifact['name'],
--- a/taskcluster/taskgraph/transforms/tests.py
+++ b/taskcluster/taskgraph/transforms/tests.py
@@ -23,22 +23,22 @@ from taskgraph.transforms.base import Tr
 from taskgraph.util.schema import resolve_keyed_by
 from taskgraph.util.treeherder import split_symbol, join_symbol
 from taskgraph.transforms.job.common import (
     docker_worker_support_vcs_checkout,
 )
 from taskgraph.util.schema import (
     validate_schema,
     optionally_keyed_by,
+    Schema,
 )
 from voluptuous import (
     Any,
     Optional,
     Required,
-    Schema,
 )
 
 import copy
 import logging
 import os.path
 import re
 
 ARTIFACT_URL = 'https://queue.taskcluster.net/v1/task/{}/artifacts/{}'
--- a/taskcluster/taskgraph/util/schema.py
+++ b/taskcluster/taskgraph/util/schema.py
@@ -2,16 +2,17 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import, print_function, unicode_literals
 
 import re
 import copy
 import pprint
+import collections
 import voluptuous
 
 
 def validate_schema(schema, obj, msg_prefix):
     """
     Validate that object satisfies schema.  If not, generate a useful exception
     beginning with msg_prefix.
     """
@@ -83,34 +84,69 @@ def resolve_keyed_by(item, field, item_n
 
     if subfield not in container:
         return item
     value = container[subfield]
     if not isinstance(value, dict) or len(value) != 1 or not value.keys()[0].startswith('by-'):
         return item
 
     keyed_by = value.keys()[0][3:]  # strip off 'by-' prefix
-    key = extra_values.get(keyed_by) if keyed_by in extra_values else item[keyed_by]
+    key = extra_values.get(
+        keyed_by) if keyed_by in extra_values else item[keyed_by]
     alternatives = value.values()[0]
 
     # exact match
     if key in alternatives:
         container[subfield] = alternatives[key]
         return item
 
     # regular expression match
-    matches = [(k, v) for k, v in alternatives.iteritems() if re.match(k + '$', key)]
+    matches = [(k, v)
+               for k, v in alternatives.iteritems() if re.match(k + '$', key)]
     if len(matches) > 1:
         raise Exception(
             "Multiple matching values for {} {!r} found while determining item {} in {}".format(
                 keyed_by, key, field, item_name))
     elif matches:
         container[subfield] = matches[0][1]
         return item
 
     # default
     if 'default' in alternatives:
         container[subfield] = alternatives['default']
         return item
 
     raise Exception(
         "No {} matching {!r} nor 'default' found while determining item {} in {}".format(
             keyed_by, key, field, item_name))
+
+
+def check_schema(schema):
+    identifier_re = re.compile('^[a-z][a-z0-9-]*$')
+
+    def iter(sch):
+        if isinstance(sch, collections.Mapping):
+            for k, v in sch.iteritems():
+                if isinstance(k, (voluptuous.Optional, voluptuous.Required)):
+                    k = str(k)
+                if isinstance(k, basestring):
+                    if not identifier_re.match(str(k)):
+                        raise RuntimeError(
+                            'YAML schemas should use dashed lower-case identifiers, '
+                            'not {!r}'.format(k))
+                iter(v)
+        elif isinstance(sch, (list, tuple)):
+            for v in sch:
+                iter(v)
+        elif isinstance(sch, voluptuous.Any):
+            for v in sch.validators:
+                iter(v)
+    iter(schema.schema)
+
+
+def Schema(*args, **kwargs):
+    """
+    Operates identically to voluptuous.Schema, but applying some taskgraph-specific checks
+    in the process.
+    """
+    schema = voluptuous.Schema(*args, **kwargs)
+    check_schema(schema)
+    return schema