GithubHelp home page GithubHelp logo

codeenigma / deployments Goto Github PK

View Code? Open in Web Editor NEW
13.0 13.0 10.0 981 KB

Our custom Fabric and shell scripts for deploying various PHP apps.

License: MIT License

Python 89.13% Shell 10.18% Ruby 0.63% Perl 0.06%

deployments's People

Contributors

angelsk avatar emlynk avatar galooph avatar gregharvey avatar instanceofjamie avatar matason avatar mdecorniquet avatar mig5 avatar nfawbert avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

deployments's Issues

Make sync jobs generic

We've had a request to make sync jobs configurable and generic via the config ini / yaml file, the idea being you should be able to specify details of your various MySQL configs (probably their cnf files with connection details in, host and path) and the scripts should use those to sync databases from %env to %env.

Similarly, we should be able to set volatile asset directory paths and sync from %env to %env without making assumptions about locations of things.

Migrate from Fabric to Robo.li

As part of starting to look into #210 I discovered that Fabric 2 basically isn't finished. And unless you're already a heavyweight Python coder it's really hard to do anything useful, because none of the 'glue' from Fabric 1 has been ported.

Enter Robo.li. For one, we're at a company full of PHP developers, and secondly it's really easy to use and really well documented.

So we're going to change tack, Fabric is dead (long live Fabric!) and I'm going to look at porting to Robo.li instead. This issue closes #210.

Config loading code is ugly

We have a big block of code like this at the top of drupal/fabfile.py:

  # Now let's fetch alterations to those defaults from config.ini, if present
  if config.has_section("Build"):
    print "=> We have some build options in config.ini"
    # Provide the path to an alternative deploy key for this project
    if config.has_option("Build", "ssh_key"):
      ssh_key = config.get("Build", "ssh_key")
      print "===> path to SSH key is %s" % ssh_key
etc.

We could do with a config loading wrapper that gets passed the config file, the config item to look up and an optional variable for those configs such as url that may have been passed in via the Jenkins execute build box. If it gets a value already, it doesn't change it - if it doesn't get a value, it looks in config.ini for one. If it finds one it returns it, otherwise it returns None.

That way we could call configs we need directly in function calls instead of having a big block of horrid code at the top of each main() function. A good example of this is in the same file with Emlyn's feature branching code:

  execute(InitialBuild.initial_build_vhost, repo, url, branch, build, alias, buildtype, FeatureBranches.ssl_enabled, FeatureBranches.ssl_cert, FeatureBranches.ssl_ip, FeatureBranches.httpauth_pass, FeatureBranches.drupal_common_config, webserverport)

It's loading the feature branch configs it needs inline in the function call, no need to pre-define them.

This would be much neater if things like ssh_key, for example, instead of being loaded in a big block at the top and then only used once, could be fetched with common.Utils.get_config(config, "ssh_key") when you need it.

Refactor behat testing code and move from drupal/DrupalTests to common/Tests

Everything that's reusable in DrupalTests for behat should move to common, as behat isn't just a Drupal thing! We should keep the module disabling and enabling behaviour separated and wrap the common behat functions with some Drupal behat functions that handle that, but the core of behat support should also work for other platforms.

Adjust symlink for Drupal should be outside of the existing_build() function

At the moment the multisite loop causes the symlink for live to be removed and recreated several times (once for each site installed/updated). It should be moved outside of the multisite loop so it happens once.

However, this is tricky because several functions that occur after it use drush aliases to run, which requires the symlink to be correct. So we need to refactor all code using drush aliases so it actually uses with cd('/path/to/drupal'): drush blah instead of drush @alias_name blah so we can safely process the symlink move later on in the script.

drushcommand script does not run correctly

When the backup parameter is passed into the script from a Jenkins job, it's passed in as a string rather than a boolean (this actually applies to all parameters).

So when backup=False is passed, the backup variable is set. That means, if when it's set to False, the following if statement is satisfied:

# Take a database backup first if told to.
if backup:
db_name = Drupal.get_db_name(shortname, branch, "default")
execute(common.MySQL.mysql_backup_db, db_name, 'drush_command', True)

The execute(common.MySQL.mysql_backup_db, db_name, 'drush_command', True) call fails to run because the app_primary role hasn't been defined:

deployments/common/MySQL.py

Lines 126 to 129 in 76df37b

# Take a database backup
@task
@roles('app_primary')
def mysql_backup_db(db_name, build, fail_build=False, mysql_config='/etc/mysql/debian.cnf'):


So, the solution here is we need ensure the backup parameter is a boolean and we need to define the app_primary role. I do not think we can use the common.Utils.define_roles() function to do this because a config.ini file does not exist. I think we can simply set that role in the fabfile-drushcommand.py script.

URL set in feature branch configuration does not work properly

We have the ability to set a URL template in config.ini, like this:

[featurebranch]
urltemplate=reponame.branchname.example.com

In FeatureBranches.py, we set the url variable like so, if the urltemplate option is present. But before that, we define the global url variable:

global ssl_cert
global drupal_common_config
global url

# Check if there's a url option under the [featurebranch] section. If there is, set
# the url variable to that value, which will override the default None value.
if config.has_option("featurebranch", "urltemplate"):
print "Feature Branch: Found a urltemplate option..."
urltemplate = config.get("featurebranch", "urltemplate")
urltemplate = urltemplate.replace("reponame", repo, 1)
urltemplate = urltemplate.replace("branchname", branch, 1)
print "urltemplate is now %s" % urltemplate
url = urltemplate

However, we do not pass in the correct variable to be used here. We need to pass in the alias variable.

Plus, I'm not sure if the URL set in the FeatureBranches.py function will stick as back in fabfile.py, the url variable is then generated:

# Set a URL if one wasn't already provided and clean it up if it was
url = common.Utils.generate_url(url, repo, branch)

I'll run some tests once I've fixed the variable being passed in issue.

Initial build can fail to find MySQL defaults file but not fail the build

This just happened on a client build:

sudo: mysql --defaults-file=/etc/mysql/debian.cnf -e 'GRANT ALL ON `DATABASE`.* TO "USER"@"172.30.0.30" IDENTIFIED BY "PASSWORD"'
out: Could not open required defaults file: /etc/mysql/debian.cnf
out: Fatal error in defaults handling. Program aborted

It didn't fail, nor did it fail later:

===> Installing Drupal with MySQL string of mysql://USER:PASSWORD@RDS_URL/DATABASE
run: drush si minimal -y --db-url=mysql://USER:PASSWORD@RDS_URL/DATABASE
out: You are about to CREATE the 'DATABASE' database. Do you want to continue? (y/n): y
out: Failed to create database: ERROR 1045 (28000): Access denied for user    [error]
out: 'USER'@'172.30.0.30' (using password: YES)

It carried right on until it eventually failed trying to alter the user 1 password.

Refactor code relying on drush site aliases

At the moment we check for the existence of a site with a drush sa command to list the available sites. However, with drush increasingly shipping with Drupal via composer and that being the desired behaviour for developers, the idea of a system-level drush, thus also system-level drush aliases, is going away.

While we can always install a system drush and figure out a way to enforce its use for alias checks, it will be an oddity in the future. The path of least resistance feels like refactoring this behaviour so it searches for sites based on link existence instead of drush site aliase presence.

Move from .ini files to .yml files for CI config

Feedback from several developers has been the INI format is obsolete and people are generally more familiar with YAML these days, so it would benefit the uptake of the scripts if config.ini became config.yml.

Obviously we'll have to support both for a time, with a view to ultimately phasing out the INI type.

mysql_version override in config.ini is not compared correctly in MySQL.py

If the mysql_version in config.ini is set, it is not compared correctly when evaluated here:

# Different MySQL versions support different string lengths for database and user name
# Set safe defaults for MySQL 5.5 or lower
db_name_length = 16
db_username_length = 16
# If MySQL version is 5.6 or higher, allow longer names
if mysql_version > 5.6:
db_name_length = 64
db_username_length = 32
elif mysql_version == 5.6:
db_name_length = 32
# Allow space for integer suffix if required
db_name_length = db_name_length - 4
print "===> MySQL version is %s, setting database name length to %s and username length to %s." % (mysql_version, db_name_length, db_username_length)

For example, in the 8.x-multisite branch of the example-drupal repo, I had:

[Database]
mysql_version=5.5

However, during an initial build of that branch, I saw the following in the Jenkins console output:

===> MySQL version is 5.5, setting database name length to 60 and username length to 32.

Which is clearly wrong, as per the code above. That value has been evaluated as greater than 5.6, when it is not.

Support Symfony initial builds with a seed database

The ultimate purposes of this is to allow Symfony projects to have feature branching.

At the moment we can only seed databases with WordPress and Drupal. Symfony presents a challenge, as by its nature the apps tend to be bespoke, so we can't know what the parameters.yml.dist attributes for the db name, username and password will be in advance, nor can we create them and safely add them to the parameters.yml include we generate.

I think, in order to achieve this, we'll need to use the database generation code (which is already in common/MySQL.py) and then have some clever way of allowing developers to tell us which attributes are to be used for the different generated database attributes, which we'll then have to pipe into the correct attributes in the generated parameters.yml file in /var/www/config - it won't be trivial!

Be a good thing to sort out though, if we do it right it'll probably help us further abstract database handling, which is only a good thing.

Drupal settings 'include' should use include, not include_once

While not common, it can happen that a static reset 'wipes' some configuration (held in settings.php), and it needs to be re-included.
Typically SimpleSamlPHP integration can break when the idp database settings can't be found.

Switching the 'include_once' to 'include' ensures all settings are always properly loaded, and I can't think of any drawback (the main settings.php is anyway very rarely included more than once).

MR on its way.

Support Selenium-based JavaScript tests via Behat

This is something of a WIP, but we're looking at supporting Selenium via Behat in the long run. We have Behat tests up and working (though they need a little refactoring). The next step is finding a consistent way to make Selenium tests work headlessly.

Make symlinks a config option

Take a look at this code:

def adjust_files_symlink(repo, branch, build, alias, site):

Instead of having various symlink functions for handling different scenarios, we could have a generic create_symlinks() function that just takes a list of links it needs to create from config.ini/yml and makes them. That way it's platform/application agnostic, and we can have Code Enigma 'defaults' for Drupal, Magento, WordPress, etc.

Drupal scripts can't handle databases with prefixed table names

On a build for a new customer we saw the secure_admin_password() function failed with the following errors:

20:54:13 ===> Setting secure username and password for uid 1 on training_toolkit site...
20:54:13 [dev4.int.codeenigma.net] run: drush sqlq "UPDATE users SET name = 'XXXXXXXXXXXXXXXXXXX' WHERE uid = 1" 
20:54:13 [dev4.int.codeenigma.net] out: Query failed.                                                            [error]
20:54:13 [dev4.int.codeenigma.net] out: 
20:54:13 
20:54:13 Warning: run() received nonzero return code 1 while executing 'drush sqlq "UPDATE users SET name = 'XXXXXXXXXXXXXXXXXXX' WHERE uid = 1"'!
20:54:13 
20:54:14 
20:54:14 [dev4.int.codeenigma.net] run: drush upwd XXXXXXXXXXXXXXXXXXX --password='XXXXXXXXXXXXXXXXXXX'
20:54:14 [dev4.int.codeenigma.net] out: exception 'Drush\User\UserListException' with message 'Unable to find    [error]
20:54:14 [dev4.int.codeenigma.net] out: a matching user for XXXXXXXXXXXXXXXXXXX.' in
20:54:14 [dev4.int.codeenigma.net] out: /opt/drush/8/vendor/drush/drush/lib/Drush/User/UserList.php:114
20:54:14 [dev4.int.codeenigma.net] out: Stack trace:
20:54:14 [dev4.int.codeenigma.net] out: #0 /opt/drush/8/vendor/drush/drush/lib/Drush/User/UserList.php(15):
20:54:14 [dev4.int.codeenigma.net] out: Drush\User\UserList::getFromParameters('XXXXXXXXXXXXXXXXXXX...')
20:54:14 [dev4.int.codeenigma.net] out: #1 /opt/drush/8/vendor/drush/drush/commands/user/user.drush.inc(355):
20:54:14 [dev4.int.codeenigma.net] out: Drush\User\UserList->__construct('XXXXXXXXXXXXXXXXXXX...')
20:54:14 [dev4.int.codeenigma.net] out: #2 [internal function]: drush_user_password('XXXXXXXXXXXXXXXXXXX...')
20:54:14 [dev4.int.codeenigma.net] out: #3 /opt/drush/8/vendor/drush/drush/includes/command.inc(422):
20:54:14 [dev4.int.codeenigma.net] out: call_user_func_array('drush_user_pass...', Array)
20:54:14 [dev4.int.codeenigma.net] out: #4 /opt/drush/8/vendor/drush/drush/includes/command.inc(231):
20:54:14 [dev4.int.codeenigma.net] out: _drush_invoke_hooks(Array, Array)
20:54:14 [dev4.int.codeenigma.net] out: #5 [internal function]: drush_command('XXXXXXXXXXXXXXXXXXX...')
20:54:14 [dev4.int.codeenigma.net] out: #6 /opt/drush/8/vendor/drush/drush/includes/command.inc(199):
20:54:14 [dev4.int.codeenigma.net] out: call_user_func_array('drush_command', Array)
20:54:14 [dev4.int.codeenigma.net] out: #7 /opt/drush/8/vendor/drush/drush/lib/Drush/Boot/BaseBoot.php(67):
20:54:14 [dev4.int.codeenigma.net] out: drush_dispatch(Array)
20:54:14 [dev4.int.codeenigma.net] out: #8 /opt/drush/8/vendor/drush/drush/includes/preflight.inc(66):
20:54:14 [dev4.int.codeenigma.net] out: Drush\Boot\BaseBoot->bootstrap_and_dispatch()
20:54:14 [dev4.int.codeenigma.net] out: #9 /opt/drush/8/vendor/drush/drush/drush.php(12): drush_main()
20:54:14 [dev4.int.codeenigma.net] out: #10 {main}
20:54:14 [dev4.int.codeenigma.net] out:

This site uses a prefix for the Drupal tables, drupal_ in this case. However, the command we run to update the username for uid/1 is:

run('drush sqlq "UPDATE users SET name = \'%s\' WHERE uid = 1"' % u1name)

In this example, users doesn't exit. It's drupal_users. As a result, we then can't update the password for that user.

So, we need a way to determine if there's a prefix for the Drupal tables. It's not a feature we ever use, hence not noticing until now.

Probably not a high priority, as only affects updating user 1's password.

Check disk space before deployment

Pretty much what it says on the tin. This isn't a priority (particularly for us at CE, as we have Nagios checking free disk space), but if Jenkins were to do a quick disk space check to make sure $freespace is available before a deployment, it could help avoid a disaster further down the line.

Make drush status commands use the --format option

From a customer:


Changes in the output of drush status in Drush 9 have broken the teardown script.

This isn't super-urgent for us, as we're probably going to back away from Drush 9 for now, it's still buggy. However, it will need sorting at some point.

Details of the issue are here: drush-ops/drush#3076

The teardown script fails at this line as a result:

dbname = sudo("drush status | grep \"Database name \" | cut -d \":\" -f 2")

We have also noticed the code before you call the obfuscate Ruby script parses drush output without using --format so that will break with Drush 9 too.

So we probably need to address this sooner rather than later!

Create a generic 'maintenance mode' page/vhost

While we use Drupal's built in maintenance mode, this isn't suitable for other types of projects. It would be instead easier to use a 'flat' html page (path overridable in config.ini with a generic default (?)), that gets switched off/on, presumably at the vhost level.

This would make the deployment process suitable for any kind of stack, and prevent having to cope up with any Drupal changes going forward.

Support multiple PHP versions

Some servers might have multiple versions of PHP installed, and we might want to specify the location of php.ini so different builds can use different versions with the CLI. PHP provides an environment variable for this, PHPRC, so we can do something like this:

def main(php_ini_file=None):
  # usual build stuff

  if php_ini_file:
    run("export PHPRC='%s'" % php_ini_file)

  # do all the CLI stuff

  if php_ini_file:
    run("export PHPRC=''")

  # restart services, etc.

Internal ticket reference r30298.

/var/www defined explicitly as the webroot

/var/www is explicitly defined as the webroot throughout these scripts.

This is fine most of the time, but does cause issues with the remove_old_builds script as on our server /var/www is a symlink to /srv/www and this causes the find command in that script to fail.

Ideally, we could pass the webroot in as a parameter (with /var/www as a fallback).

I'll try and raise a PR for this if/when I get time, but there are about 300 or so instances that will need changing!

Move composer functionality to "common"

As Composer is increasingly becoming the way of building PHP projects (Magento, SimpleSAMLphp, Drupal > 7, Symfony, etc.) it makes sense to make our Composer bits central and commonly available.

Add support for Drush CMI Tools

We can detect the inclusion of Drush CMI Tools in a project as follows:

drush @site_alias help --format=yaml | grep "config-import-plus"

If that grep returns results then the Drush module is installed, and if it is installed we can safely assume it should be used and drush cimy should be preferred over drush cim.

Autoscale builds should not remove initial buildtype.settings.php files

When the autoscale cluster is rebuilt or a new EC2 instance is added, the webroot is now put into /var/www/project_branch_build_0, rather than just /var/www/project.

This means we don't need to remove the files at sites/default/*.settings.php during a deployment, nor should we. We had a case whereby the first deployment after the cluster had been rebuilt failed, so when Jenkins reverted the include_once() statement in settings.php, it referenced files that no longer existed, which resulted in a site that was unavailable.

Add ability to perform a regex check after a build

It would be handy to have the ability to run a regex check on completion of a build (mainly production builds). If said regex isn't found, either the build could be reverted (though I'm not sure about this) or a hard-to-miss message displayed in the console output...maybe even an email being sent to a predefined address.

An example of hard-to-miss text would be:

################################
################################
################################

REGEX TEST FOR {site_name} FAILED! MANUAL CHECK REQUIRED.

################################
################################
################################

Or something like that.

Reverting settings.php uses wrong value

When a build fails and the per-buildtype settings.php config in settings.php needs to be reverted, Revert.py currently uses the branch name rather than buildtype. If the repo has been setup whereby the branch and buildtype values don't match (i.e the production branch is master but the buildtype is prod), the correct overriding file is not included.

A fix for this will require updating the _revert_settings() function in Revert.py, along with making sure any function that function is called from has the buildtype parameter passed to it.

Make 'jenkins' username configurable

It's entirely feasible someone might use these scrips and not use Jenkins, so actually the CI username should be something you can configure which defaults to 'jenkins' - otherwise folk using other systems, or just bash scripts, will be obliged to create a 'jenkins' user.

Teardowns fail when "drush status" returns !=0

I've noticed some teardowns failing when drush status returns non-zero.
The code falls back to trying to extract the database name from settings.php:

dbname = sudo("grep \"'database' => '%s*\" settings.php | cut -d \">\" -f 2" % alias)

But that never succeeds because it greps for a database name based on the alias. However database names rarely match the alias because their names are deliberately truncated on creation:

# Different MySQL versions support different string lengths for database and user name
# Set safe defaults for MySQL 5.5 or lower
db_name_length = 16
db_username_length = 16
# If MySQL version is 5.6 or higher, allow longer names
if mysql_version > 5.6:
db_name_length = 64
db_username_length = 32
elif mysql_version == 5.6:
db_name_length = 32
# Allow space for integer suffix if required
db_name_length = db_name_length - 4
print "===> MySQL version is %s, setting database name length to %s and username length to %s." % (mysql_version, db_name_length, db_username_length)

A fix to get it to grep for the right thing would be complicated, as we'd need to know what the truncation was.

As a slight aside, in this case even though drush status was returning non-zero, it was still outputting the "Database name" correctly. So perhaps a good first fix would be not to immediately fail on non-zero, but rather check the output first.

Running phpunit with PHP 5

Referring to this code in common/Tests.py

# We cannot make phpunit work with PHP 5.x, too many problems
# Detect PHP version
php_version = run("php -v | head -1 | awk {'print $2'} | cut -d. -f1,2")
if any(version in php_version for version in [ '5.3', '5.4', '5.5', '5.6' ]):
  # Sorry, PHP too old!
  print "##### Sorry, this version of PHP is too old for current phpunit builds, we cannot run the tests."
  return phpunit_tests_failed

What are the issues getting this to run? I've able to do this locally with PHP 5.6 by running a command like cd www && ../vendor/bin/phpunit -c core modules/custom and ideally I'd be able to run them during the build to identify and regressions and fail the build automatically if a test fails.

Support LetsEncrypt SSL certs for new builds

Would be nice to provide a LetsEncrypt certificate automatically for SSL as part of the initial build behaviour, if SSL is required and no specific certificate is specified.

Revert 'quick help' command is not properly populated

At the moment the output looks like this:

sudo /home/jenkins/revert -b  -d   -s /var/www/live.website.live -a website_live

When it should be:

sudo /home/jenkins/revert -b /var/www/website_branch_build_109 -d /home/jenkins/dbbackups/website_prior_to_build_110.sql.gz -s /var/www/live.website.live -a website_branch

Environment Indicator settings for Drupal can't be overridden

We automatically add Environment Indicator colours if the module is present and turn it on. However, it has been noted the variables are injected into settings.inc after the per-buildtype settings include, so they cannot be overridden. This is not desired, it should be possible to override in branchname.settings.php in the repository.

drupal_version should be cast as an integer

We should be treating this as a proper integer, so that instead of things like drupal_version == '8' we should be able to do things like drupal_version > 7 - otherwise we'll have unnecessary complications / problems with Drupal 9.

Fabric 1.x -> 2.x upgrade

It's the big one! But we got to do it... :-)

I'll set up a feature branch for this in the mainline repo and also get Python 3/Fabric 2 installed on a staging server so we can start tackling this.

Nginx / Apache configurations should be handled by deployment scripts

Historically we've depended on Puppet to put the necessary Nginx and Apache configurations in place. We should change this so the deploy scripts place the configs if they're not there. We should make the vhost config location a variable while we're at it, and put these configs in the same directory.

Apache feature branch deployments lack the same level of enhancements as Nginx

When the feature branch functionality was initially built, it was primarily done for Nginx webservers. However, the same level of functionality should be made available for Apache webservers.

For example:

  • The ability to add HTTP auth to the feature branch being deployed
  • The ability to use SSL on the new feature branch

The solution will be to create dummy Apache vhosts, much like the Nginx ones, which can be used during the initial deployment of a feature branch.

Multisite reverts only revert the site that broke

If you have a multisite architecture and one of the sites breaks, the script will stop and revert the database and settings for the site that failed but not any others it previously built in the process.

We'll need to somehow build a list of already 'treated' sites in a build and go back and revert each one in turn in the event of a failure further down the line.

Also, when drush updates fail for multisites it does not revert the link to the previous build directory.

So there are a few logical flaws to overcome for rolling back multisite builds. Should probably be tackled as a single refactoring job of revert behaviour to make it handle multisites and associated generic links.

Live symlinks and settings.php reverts only happen on primary app server

If the server layout involves more than one app server, should the build fail, the live symlink will only be adjusted on the primary app server. The same applies to when the settings.php file is reverted to include the $buildtype.settings.php file from the previous working build.

Take the config_import() function as an example:

# Function used by Drupal 8 builds to import site config
@task
@roles('app_primary')
def config_import(repo, branch, build, site, alias, drupal_version, previous_build):
db_name = get_db_name(repo, branch, site)
with settings(warn_only=True):
# Check to see if this is a Drupal 8 build
if drupal_version > 7:
print "===> Importing configuration for Drupal 8 site..."
if sudo("su -s /bin/bash www-data -c 'cd /var/www/%s_%s_%s/www/sites/%s && drush -y cim'" % (repo, branch, build, site)).failed:
print "Could not import configuration! Reverting this database and settings"
sudo("unlink /var/www/live.%s.%s" % (repo, branch))
sudo("ln -s %s /var/www/live.%s.%s" % (previous_build, repo, branch))
common.MySQL.mysql_revert_db(db_name, build)
Revert._revert_settings(repo, branch, build, site, alias)
raise SystemExit("Could not import configuration! Reverted database and settings. Site remains on previous build")
else:
print "===> Configuration imported. Running a cache rebuild..."
drush_clear_cache(repo, branch, build, site, drupal_version)

That function is only run on the primary app server, so when Revert._revert_settings(repo, branch, build, site, alias) is run, it's only run on what has been designated the primary app server.

So we need a way to run the revert functions on all app servers, if there are more than one.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.