def publish(version, stage, config_file):
    """Publish a version of the project to a stage."""
    config = _load_config(config_file)
    cfn = boto3.client('cloudformation')

    if version is None:
        version = config['devstage']
    elif version not in config['stage_environments'].keys() and \
            not version.isdigit():
        raise ValueError('Invalid version. Use a stage name or a numeric '
                         'version number.')
    if version == stage:
        raise ValueError('Cannot deploy a stage into itself.')

    # obtain previous deployment
    try:
        previous_deployment = cfn.describe_stacks(
            StackName=config['name'])['Stacks'][0]
    except botocore.exceptions.ClientError:
        raise RuntimeError('This project has not been deployed yet.')

    # preserve package from previous deployment
    bucket = _get_from_stack(previous_deployment, 'Parameter',
                             'LambdaS3Bucket')
    lambda_package = _get_from_stack(previous_deployment, 'Parameter',
                                     'LambdaS3Key')

    # prepare cloudformation template
    template_body = get_cfn_template(config)
    parameters = [
        {'ParameterKey': 'LambdaS3Bucket', 'ParameterValue': bucket},
        {'ParameterKey': 'LambdaS3Key', 'ParameterValue': lambda_package},
    ]
    stages = list(config['stage_environments'].keys())
    stages.sort()
    for s in stages:
        param = s.title() + 'Version'
        if s != stage:
            v = _get_from_stack(previous_deployment, 'Parameter', param) \
                if previous_deployment else '$LATEST'
            v = v or '$LATEST'
        else:
            if version.isdigit():
                # explicit version number
                v = version
            else:
                # publish version from a stage
                v = _get_from_stack(previous_deployment, 'Parameter',
                                    version.title() + 'Version')
                if v == '$LATEST':
                    # publish a new version from $LATEST
                    lmb = boto3.client('lambda')
                    v = lmb.publish_version(FunctionName=_get_from_stack(
                        previous_deployment, 'Output', 'FunctionArn'))[
                            'Version']
        parameters.append({'ParameterKey': param, 'ParameterValue': v})

    # run the cloudformation template
    print('Publishing {}:{} to {}...'.format(config['name'], version, stage))
    cfn.update_stack(StackName=config['name'], TemplateBody=template_body,
                     Parameters=parameters,
                     Capabilities=['CAPABILITY_IAM'])
    waiter = cfn.get_waiter('stack_update_complete')

    # wait for cloudformation to do its thing
    try:
        waiter.wait(StackName=config['name'])
    except botocore.exceptions.ClientError:
        raise

    # we are done, show status info and exit
    _print_status(config)