Giter Club home page Giter Club logo

jira-bugzilla-integration's Introduction

Status Sustain Build Docker image Run tests pre-commit

Jira Bugzilla Integration (JBI)

System to sync Bugzilla bugs to Jira issues.

Caveats

  • The system accepts webhook events from Bugzilla
  • Bugs' whiteboard tags are used to determine if they should be synchronized or ignored
    • Only public bugs are eligible for sychronization.
  • The events are transformed into Jira issues
  • The system sets the see_also field of the Bugzilla bug with the URL to the Jira issue
    • No other information is sychronized from Jira to Bugzilla.

Note: whiteboard tags are string between brackets, and can have prefixes/suffixes using dashes (eg. [project], [project-fx-h2], [backlog-project]).

Diagram Overview

graph TD
    subgraph bugzilla services
        A[Bugzilla] -.-|bugzilla event| B[(Webhook Queue)]
        B --- C[Webhook Push Service]
    end
    D --> |create/update/delete issue| E[Jira]
    D<-->|read bug| A
    D -->|update see_also| A
    subgraph jira-bugzilla-integration
        C -.->|post /bugzilla_webhook| D{JBI}
        F["config.{ENV}.yaml"] ---| read actions config| D
    end

Documentation

Usage

How to onboard a new project?

  1. Submit configuration for your project

  2. Grant permissions to the Jira Automation Bot

    • If you are an admin of the Jira project

      • go to your Jira project and open Project Settings, then People.
      • Select Add People and search for Jira Automation. If two are listed select the one with the green logo
      • From the Roles drop down select Bots. Click Add 1 person.
      • Add these permissions for the bot
         "ADD_COMMENTS",
         "CREATE_ISSUES",
         "DELETE_ISSUES",
         "EDIT_ISSUES"
      
    • If you are not an admin of the Jira project, contact the admin or reach out to #jira-support in Slack to determine how best to request the changes described above

  3. Once your configuration is merged and a JBI release is deployed, create a bug in Bugzilla and add your whiteboard tag to the bug. Note that the tag must be enclosed in square brackets (eg. [famous-project]). You should see an issue appear in Jira

    • If you want to start syncing a bug to a Jira issue that already exists, add the issue's link to the See Also section of the Bugzilla bug before you add the whiteboard tag
  4. Verify that the action you took on the bug was property reflected on the Jira issue (e.g. the description was updated or a comment was added)

Development

We use pandoc to convert markdown to the Jira syntax. Make sure the binary is found in path or specify your custom location.

  • make start: run the application locally (http://localhost:8000)
  • make test: run the unit tests suites
  • make lint: static analysis of the code base
  • make format: automatically format code to align to linting standards

In order to pass arguments to pytest:

poetry run pytest -vv -k test_bugzilla_list_webhooks

You may consider:

  • Tweaking the application settings in the .env file (See jbi/environment.py for details)
  • Installing a pre-commit hook to lint your changes with pre-commit install

jira-bugzilla-integration's People

Contributors

alexcottner avatar bsieber-mozilla avatar cbellini avatar cpeterso avatar danielnguyen-mozilla avatar davehunt avatar ddurst avatar dependabot[bot] avatar dmose avatar gijsk avatar gleonard-m avatar grahamalama avatar hwine avatar jchaupitre avatar leplatrem avatar mossop avatar muffinresearch avatar onemahon avatar scholtzan avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

jira-bugzilla-integration's Issues

Usage metrics for JBI

Context: Feedback from the ITEO program review

  • metrics for each configured action about how many syncs it sent from BZ to Jira
  • can go to grafana
  • gives us an idea of how much manual work JBI has saved / automated

Jinja2 Templating?

Design Endpoint/API that can replace generate-helper.sh?

  • Is this worth it?

HTML Jinja2 Template for Powered by JBI

Code coverage error

Traceback (most recent call last):
  File "/opt/pysetup/.venv/lib/python3.9/site-packages/coverage/cmdline.py", line 746, in do_run
    runner.run()
  File "/opt/pysetup/.venv/lib/python3.9/site-packages/coverage/execfile.py", line 247, in run
    exec(code, main_mod.__dict__)
  File "/opt/pysetup/.venv/lib/python3.9/site-packages/pytest/__main__.py", line 5, in <module>
    raise SystemExit(pytest.console_main())
SystemExit: ExitCode.OK

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/opt/pysetup/.venv/bin/coverage", line 8, in <module>
    sys.exit(main())
  File "/opt/pysetup/.venv/lib/python3.9/site-packages/coverage/cmdline.py", line 871, in main
    status = CoverageScript().command_line(argv)
  File "/opt/pysetup/.venv/lib/python3.9/site-packages/coverage/cmdline.py", line 588, in command_line
    return self.do_run(options, args)
  File "/opt/pysetup/.venv/lib/python3.9/site-packages/coverage/cmdline.py", line 753, in do_run
    self.coverage.save()
  File "/opt/pysetup/.venv/lib/python3.9/site-packages/coverage/control.py", line 659, in save
    data = self.get_data()
  File "/opt/pysetup/.venv/lib/python3.9/site-packages/coverage/control.py", line 727, in get_data
    if self._collector and self._collector.flush_data():
  File "/opt/pysetup/.venv/lib/python3.9/site-packages/coverage/collector.py", line 442, in flush_data
    self.covdata.add_lines(self.mapped_file_dict(self.data))
  File "/opt/pysetup/.venv/lib/python3.9/site-packages/coverage/sqldata.py", line 438, in add_lines
    self._choose_lines_or_arcs(lines=True)
  File "/opt/pysetup/.venv/lib/python3.9/site-packages/coverage/sqldata.py", line 495, in _choose_lines_or_arcs
    with self._connect() as con:
  File "/opt/pysetup/.venv/lib/python3.9/site-packages/coverage/sqldata.py", line 300, in _connect
    self._create_db()
  File "/opt/pysetup/.venv/lib/python3.9/site-packages/coverage/sqldata.py", line 249, in _create_db
    with db:
  File "/opt/pysetup/.venv/lib/python3.9/site-packages/coverage/sqldata.py", line 1037, in __enter__
    self._connect()
  File "/opt/pysetup/.venv/lib/python3.9/site-packages/coverage/sqldata.py", line 1019, in _connect
    self.con = sqlite3.connect(filename, check_same_thread=False)
sqlite3.OperationalError: unable to open database file
ERROR: 1
make: *** [test] Error 1

Create a test case that covers #136 and #137

We saw odd serialization behavior in #136 and #137 that we closed with #139 and #142.

We only verified our fixes with manual testing though -- ideally this would be covered by a test case or two.

We should also investigate why the model-level exclude config didn't seem to have the desired outcome (we had to use the dict(exclude={...} method)

Remove `development` stage from Dockerfile

Since our development environment is relatively easy to set up to run tests & linting, and those things are not run through Docker in CI anymore (since #88), we should remove the development stage of the Dockerfile. We should still configure a way to run the production container in "development" mode through environment variables and/or uvicorn.run() params.

Migrate Default's into Action Configuration YAML?

The following lines establish defaults for a a few of the action config fields:

inner_action_dict.setdefault("action", "src.jbi.whiteboard_actions.default")
inner_action_dict.setdefault("enabled", False)
inner_action_dict.setdefault("parameters", {})

Perhaps a new section in the config.*.yaml could be added to control this from configuration.

---
# Action Config
actions:
devtest:

Should there be a new section? Maybe there's an alternate route to explore here

Should this new section be: default, default.actions, actions._default, or actions.default.

  • default: as a top-level section
  • default.actions: a top-level section that has a section dedicated to the other top-level section action
  • actions._default: adding the underscore should ensure yamllint keeps this element at the top of the list (we can also remove ordering of keys from yamllint config)
  • actions.default 🤷

google-github-actions: not allowed in github org

google-github-actions/setup-gcloud@v0 is not allowed to be used in mozilla/jira-bugzilla-integration. Actions in this workflow must be: within a repository that belongs to your Enterprise account, created by GitHub or match the following: !/mozilla/**, !mozilla/**, ./**, 10up/wpcs-action@*, aws-actions/*, docker/*, pypa/[email protected], slackapi/slack-github-action@*, codecov/codecov-action@v2.

EDIT:
Link to bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1759241

Can we ping Bugzilla to see if the webhook is enabled?

We've experienced a few system disruptions caused by Bugzilla disabling the webhook that we've set up to push events to our service.

Is there a way for us to send some request to Bugzilla to check on the status of the webhook? If so, this is something we could potentially incorporate into the healthcheck endpoint.

Crash with `Type is not JSON serializable: DateTime`

Context: https://sentry.io/organizations/mozilla/issues/3416994548/?environment=stage&project=6364263

This happens in ORJson, introduced in #86

  File "src/app/api.py", line 101, in bugzilla_webhook
    return ORJSONResponse(content=result, status_code=200)
  File "starlette/responses.py", line 49, in __init__
    self.body = self.render(content)
  File "fastapi/responses.py", line 34, in render
    return orjson.dumps(content)

Probably from a Jira response object.

Run app as a single process in production

From the FastAPI docs:

If you have a cluster of machines with Kubernetes (...) then you will probably want to handle replication at the cluster level instead of using a process manager (like Gunicorn with workers) in each container.
...
In those cases, you would probably want to (...) [run a] single Uvicorn process instead of running something like Gunicorn with Uvicorn workers.

I think this means we should:

  • change the default container CMD to run uvicorn
  • remove gunicorn_conf.py

Exception in JBI can delay webhook delivery

If JBI returns some kind of server error (500 code) to Bugzilla when it tries to deliver a webhook event then Bugzilla keeps retrying that event, delaying delivery of any new events that occur. It has some kind of exponential back-off on retries. After five failures it waits 15 minutes between each attempt. If the webhook fails a certain number of times (100 by default but can be changed in bugzilla's config) then the webhook is just disabled entirely and the owner emailed. In our case I don't think the user's email address is valid so I suspect we just wouldn't hear about it.

The result of this is that if something goes wrong inside JBI, maybe the Jira API is down or something then the event will be retried (this would seem to resolve #33). But if something more permanent goes wrong, say a bug in how we parse the webhook payload or something, then it will completely block syncing.

This seems troublesome. The retrying is good as long as the problem is temporary but I wonder if that is something we should handle internally (as #33 seems to be talking about) rather than completely blocking later events from being processed.

Do we have any monitoring set up that would alert us on server failures?

Enable Precommit for *Gunicorn_conf.py

pre-commit/pre-commit#1966

When trying to run pre-commit on my local machine something like the above issue kept occurring. I had not modified the files but they kept appearing. Even when allowing them to go through pre-commit and get "fixed" then would fail the next run and then pass again. I'm not sure if it's a mac issue or pre-commit issue.

I had to put it in both locations because iSort ignored the top one.

Originally posted by @bsieber-mozilla in #27 (comment)

`make lint` is too slow

make lint 3.54s user 1.93s system 10% cpu 51.279 total

Shouldn't we run pre-commit run --all-files instead ? (98.66s user 5.41s system 522% cpu 19.920 total)

Improving structured logging

Looking at JBI logs, it is very hard to debug/troubleshoot a use-case.

When filtering the logs on jsonPayload.Type!="request.summary" or jsonPayload.Type="ignored-requests" we obtain this sort of entry:

{
  "insertId": "1vxv4qutgqc8iupq",
  "jsonPayload": {
    "Fields": {
      "msg": "request: {\"webhook_id\": 22, \"webhook_name\": \"JBI-PROD-InfrastructureAndOperations\", \"event\": {\"action\": \"modify\", \"time\": \"2022-06-30T15:05:07\", \"user\": {\"id\": yyyyyy, \"login\": \"[email protected]\", \"real_name\": \"XXXXX\"}, \"changes\": [{\"field\": \"flag.needinfo\", \"removed\": \"\", \"added\": \"? ([email protected])\"}], \"target\": \"bug\", \"routing_key\": \"bug.modify:flag.needinfo\"}, \"bug\": {\"id\": XXXX, \"is_private\": false, \"type\": \"task\", \"product\": \"Infrastructure & Operations\", \"component\": \"Infrastructure: LDAP\", \"whiteboard\": \"\", \"keywords\": [], \"flags\": [{\"id\": 2114317, \"name\": \"needinfo\", \"requestee\": {\"id\": yyyyy, \"login\": \"xxxxxxxxxx\", \"real_name\": \"XXX XXXX\"}, \"value\": \"?\"}], \"status\": \"ASSIGNED\", \"resolution\": \"\", \"see_also\": [], \"summary\": \"Restore Commit Access (Level 3) for XXXXX\", \"severity\": \"--\", \"priority\": \"\", \"creator\": \"[email protected]\", \"assigned_to\": \"[email protected]\", \"comment\": null}}"
    },
    "Hostname": "jbi-prod-jbi-app-1-6799547446-xlnf4",
    "Severity": 6,
    "Logger": "jbi",
    "Type": "src.jbi.router",
    "EnvVersion": "2.0",
    "Pid": 11,
    "Timestamp": 1656601536174810000
  },
}

I propose that we use structured logs better:

  • msg: a short human message
  • operation: an enum of predefined actions (eg. create, update, comment, ignore ...)
  • action: the matching configured action and its parameters
  • requestBody: the input JSON
  • responseBody: the output JSON

This way we could track bug numbers better with log queries like:

jsonPayload.Type="ignored-requests"
jsonPayload.Fields.requestBody.bug.id=12345678

Actions validation assumes an action has a `whiteboard_tag` parameter

In our Action model, we specify

class Action(YamlModel):
   # ...
   parameters: dict = {}
   # ...

Yet in the Actions model, we have this validation logic:

for name, action in actions.items():
    if name.lower() != action.parameters["whiteboard_tag"]:
        raise ValueError("action name must match whiteboard tag")

This makes the implicit assumption that every Action have a whiteboard_tag.

We should either change our config schema so a whiteboard_tag is a required parameter, or modify this logic somehow.

`python -m src.app.api` fails to run

Since src.app.api has a main, we should be able to run the app locally...

$ JIRA_USERNAME=foo JIRA_API_KEY=bar BUGZILLA_API_KEY=bz PORT=8000 poetry run python -m src.app.api

Traceback (most recent call last):
  File "/opt/homebrew/Cellar/[email protected]/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/runpy.py", line 197, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/opt/homebrew/Cellar/[email protected]/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "/Users/mathieu/Code/Mozilla/jira-bugzilla-integration/src/app/api.py", line 143, in <module>
    uvicorn.run(
  File "/Users/mathieu/Library/Caches/pypoetry/virtualenvs/jira-bugzilla-integration-fgdRkTUv-py3.9/lib/python3.9/site-packages/uvicorn/main.py", line 457, in run
    sock = config.bind_socket()
  File "/Users/mathieu/Library/Caches/pypoetry/virtualenvs/jira-bugzilla-integration-fgdRkTUv-py3.9/lib/python3.9/site-packages/uvicorn/config.py", line 551, in bind_socket
    sock.bind((self.host, self.port))
TypeError: an integer is required (got type str)

It looks like environment.py does not have the right types.

I also tried to run the app locally through Gunicorn.

$ poetry run gunicorn -k uvicorn.workers.UvicornWorker -c bin/gunicorn_conf.py src.app.api:app
...
  File "/Users/mathieu/Library/Caches/pypoetry/virtualenvs/jira-bugzilla-integration-fgdRkTUv-py3.9/lib/python3.9/site-packages/gunicorn/workers/workertmp.py", line 22, in __init__
    raise RuntimeError("%s doesn't exist. Can't create workertmp." % fdir)
RuntimeError: /dev/shm doesn't exist. Can't create workertmp.

After a quick fix in gunicorn_conf.py to remove the hard-coded value, I could get Gunicorn to work locally:

$ JIRA_USERNAME=foo JIRA_API_KEY=bar BUGZILLA_API_KEY=bz WORKER_TMP_DIR=/tmp/ poetry run gunicorn -k uvicorn.workers.UvicornWorker -c bin/gunicorn_conf.py src.app.api:app
..
[2022-07-13 15:42:12 +0200] [28107] [INFO] Starting gunicorn 20.1.0
[2022-07-13 15:42:12 +0200] [28107] [INFO] Listening at: http://0.0.0.0:8000 (28107)
[2022-07-13 15:42:12 +0200] [28107] [INFO] Using worker: uvicorn.workers.UvicornWorker
[2022-07-13 15:42:12 +0200] [28109] [INFO] Booting worker with pid: 28109
[2022-07-13 15:42:12 +0200] [28110] [INFO] Booting worker with pid: 28110
...

So, should we keep if __name__ == "__main__" block in src/app/api.py and its related settings in src/environment.py?

Migrate to PathLib

    info = {}
    version_path = Path(__file__).parents[2] / "version.json"
    if version_path.exists():
        info = json.loads(version_path.read_text(encoding="utf8"))

Don't know if I got that quite right, but the general comment is that Pathlib might be nice here

Originally posted by @grahamalama in #1 (comment)

Container not built nor ran with current host user id

make test fails with:

Creating jira-bugzilla-integration_web_run ... done
/docker-entrypoint.sh: 15: exec: bin/test.sh: Permission denied
ERROR: 126
make: *** [test] Error 126

The current user id is not passed as build arg (id -u), and the host user id does not match the container app id (10001).

How to extend existing actions?

In #75 we are adding more logic to the default action behavior. Namely setting assignee and status fields.

The chosen approach was to extend the default action behavior using inheritance.

Pros

  • No duplicated code between actions

Cons

  • Indirection. One has to read and mentally merge two classes in order to understand the final behavior
  • Hard to pick (eg. I want status to be set, but not the assignee)

There could be other approaches to consider:

Single action with (many) parameters

    action: src.jbi.whiteboard_actions.default_action
    contact: [[email protected]]
    description: example configuration
    enabled: true
    parameters:
      jira_project_key: EXMPL
      whiteboard_tag: example
      sync_assignee: True
      status_map:
        NEW: "In Progress"
        FIXED: "Closed"

Pros

  • A single piece of code to read (default_action.py)

Cons

  • The default action code could potentially grow indefinitely, and we would loose the extensibility offered by the action API
  • Unit tests have to combine a lot of combinations for the parameters

Support execution of multiple atomic actions

Instead of a single action, define a list of actions to be executed. The specified parameters would be passed to all to simplify configuration.

    contact: [[email protected]]
    description: example configuration
    enabled: true
    actions:
       - src.jbi.whiteboard_actions.create_or_update
       - src.jbi.whiteboard_actions.comments
       - src.jbi.whiteboard_actions.assignee
       - src.jbi.whiteboard_actions.status
    parameters:
      jira_project_key: EXMPL
      whiteboard_tag: example
      status_map:
        NEW: "In Progress"
        FIXED: "Closed"

Pros

  • Smaller bits of code
  • Modularity
  • Easier testing

Cons

  • Potential duplication in config if all use-cases want the same set of actions
  • Multiple calls to API for separate fields (verbose history in jira?)

There could be other approaches. Maybe the current one is fine, but at least this issue could help us drive a conversation about it (before the code would grow too much)

Check that configured projects exist in heartbeat endpoint

We are currently in a situation in STAGE where some configured projects are not visible to the Jira client.

I think it's reasonable to receive an alert as soon as possible in that case.

We could have a startup check, but it would be safer to include it in the heartbeat, so that we receive an alert if a project is deleted (without JBI restarting)

Label/Whiteboard Design - Labels with Spaces are not allowed in Jira

There was an error spotted by @mixedpuppy on slack here:
https://mozilla.slack.com/archives/C016BTJKM9B/p1645555504432889

After a diagnostic session it was determined that “spaces” (“ “) are causing an error for Jira.

Spaces can be converted to an alternate character (perhaps a . or -) or labels with spaces can be skipped.I’m leaning towards using a . when spaces are found in the tag.

https://mozilla-hub.atlassian.net/jira/software/c/projects/OSS/boards/157/backlog?view=detail&selectedIssue=OSS-474&epics=visible&issueLimit=100&selectedEpic=OSS-388

Improve logging output

When pairing with @leplatrem yesterday, we noticed a few things about our current logging output:

  • Exceptions weren't being logged to stdout
  • the root logger was seemingly overwritten by some other logger

For this issue, we should investigate our logging configuration and ensure that:

  • all relevant logs are emitted
  • all logs are handled by dockerflow.logging.JsonLogFormatter
  • access logs are disabled
  • there are no duplicate logs

Provide metrics to measure the amount and measure the type of work done by JBI

I'm interesting in being able to measure how much work manual JBI is automating away for us.

I believe the numbers I need for this are:

  • number of bugzilla operations done, segmented by type and status (success, failed, etc)
  • number of jira operations done, segmented by type and status (success, failed, etc)
  • I would also like to sum up those values into a monthly count

Questions:

  • how much effort would it take to make that happen?
  • any recommendations on different metrics?

Report errors to Sentry

We want to report stacktraces to Sentry.

Configure Sentry from a SENTRY_DSN env var. Skip if empty.

Black and iSort - Compatibility

Pre-commit and "make lint" (which runs the lint target of the dockerfile) are the two paths in which iSort and Black are used.

The intent is that both paths can continue to use the came configuration files without incurring a circular issue (where Black "solves" an issue, and iSort then views that solution as an issue; and vice versa).

Consolidate configuration per environment

Following #111 (comment)

I guess it would imply having a config.local.yaml. But it would be better than having the devtest action used in unit tests in config.nonprod.yaml
Member Author

@grahamalama grahamalama 4 days ago
Yeah, maybe this warrants an issue to decide what we want to do here. Feels like we need to account for our different environments (for development, nonprod, and prod) and decide what will be different about each environment
Member

@bsieber-mozilla bsieber-mozilla 15 hours ago
Alternatively, we opt for one config and make the chosen environments a configuration parameter.

Then the local/nonprod/prod actions would be in a consolidated yaml but enabled/disabled through whichever parameter. (Boolean flag per environment, list of all environments, etc?)

Option #1 : add a local config

  • config/config.local.yaml
  • config/config.nonprod.yaml
  • config/config.prod.yaml

Option #2 : add an environments field to action config

    whiteboard_tag: example
    contact: [email protected]
    description: example configuration
    module: src.jbi.whiteboard_actions.default
    environments: ['nonprod', 'prod']
    parameters:
      jira_project_key: EXMPL

Add StatsD (or Prometheus) metrics

  • Count accepted / rejected BZ events
  • Measure API calls latency
  • Measure execution time

Configure client from STATSD_DSN env var. Skip if empty.

Update testing approach: `MockBugzillaClient` or `MockJiraClient`

If we have to do many more tests with these services, monkeypatching for each could become cumbersome. Not something to handle in this PR, but it might be worth exploring a dependency injection approach for requiring different services in actions. That way, we could provide some MockBugzillaClient or MockJiraClient to the actions when we test them.

Originally posted by @grahamalama in #31 (comment)

How does JBI handle HTTP 429 from Bugzilla?

When we enabled this in prod, Bugzilla returned 429 Client Error: Too Many Requests for url: https://bugzilla.mozilla.org/xmlrpc.cgi.

Questions

  • does JBI recover from these gracefully?
  • are these queued somewhere for retry?

`get_jira()` and `get_bugzilla()` should be cached

Otherwise, a session is established on every instantiation. For example GET __heartbeat__ takes more than 5 seconds on each call!

For actions, since the callable (ActionExecutor instance) is cached, it is already indirectly cached

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.