diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..31ca173 --- /dev/null +++ b/.envrc @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# ^ added for shellcheck and file-type detection + +# Watch & reload direnv on change +watch_file devshell.toml + +if [[ $(type -t use_flake) != function ]]; then + echo "ERROR: use_flake function missing." + echo "Please update direnv to v2.30.0 or later." + exit 1 +fi +use flake \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0474498 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.direnv/ \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3492008 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +all: README.html README.pdf + +README.html: README.md + go run . + +README.pdf: README.md + go run . diff --git a/README.html b/README.html new file mode 100644 index 0000000..385d8d3 --- /dev/null +++ b/README.html @@ -0,0 +1,1406 @@ + + + +

Robert Cambridge

+

Self-educated backend and infrastructure software engineer.
+Passionate about readable, scalable & secure code.
+Commander of git, Terraform, Go, Typescript on node.js, nix, linux systems, networking.

+
+

"takes a lot of pride in delivering good work" - Antti Kupila

+
+ +
+

"he really gave it 110% and was critical to the project's success" - Eaden McKee

+
+ +
+

"highly skilled, battle-tested and a great team lead and mentor" - Saemie Chouchane

+
+ +
+

"one of the most naturally talented programmers you'll meet" - Jamie Learmonth

+
+

Available for freelance work in Amsterdam or remote.
+📞 +31 203 699 866 [call now]
+✉️ robert@cambridge.me
+🔗 linkedin.com/in/rcambrj
+👥 Schedule an interview with me

+

Experience

+

Frasers Group via Lab Digital, Utrecht [03/2024 - 10/2024]

+ +

VanMoof via MA Micro Ltd, Amsterdam [10/2023 - 01/2024]

+ +

VanMoof, Amsterdam [07/2021 - 05/2023]

+ +

Polestar via code d'azur, Amsterdam [09/2020 - 07/2021]

+ +

PON Occasions via Dept, Amsterdam [01/2020 - 06/2020]

+ +

[ABN AMRO Incubator] via Dept, Amsterdam [01/2020 - 02/2020]

+ +

Etos via Dept, Amsterdam [09/2019 - 12/2019]

+ +

Activia via AKQA, Amsterdam [05/2019 - 08/2019]

+ +

Imperial Tobacco/Fontem (blu.com), Amsterdam [01/2019 - 04/2019]

+ +

Ace & Tate, Amsterdam [10/2018 - 12/2018]

+ +

Gain Theory via Tweag IO, London [03/2018 - 07/2018]

+ +

Wonderbill via Boston Consulting Group, London [07/2016 - 02/2018]

+ +

NHS England via Web Technology Group, London [01/2016 - 06/2016]

+ +

Arcadia Group, London [08/2015 - 12/2015]

+ +

Square Enix, London [03/2015 - 06/2015]

+ +

Expedia, London [12/2014 - 02/2015]

+ +

Square Enix, London [05/2014 - 10/2014]

+ +

Sportlobster, London [01/2014 - 03/2014]

+ +

Boxlight, London [10/2010 - 12/2013]

+
+ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..57013d4 --- /dev/null +++ b/README.md @@ -0,0 +1,148 @@ +# Robert Cambridge + +Self-educated backend and infrastructure software engineer. +Passionate about _readable_, _scalable_ & _secure_ code. +Commander of `git`, `Terraform`, `Go`, `Typescript` on `node.js`, `nix`, `linux` systems, networking. + +> *"takes a lot of pride in delivering good work"* - Antti Kupila + + + +> *"he really gave it 110% and was critical to the project's success"* - Eaden McKee + + + +> *"highly skilled, battle-tested and a great team lead and mentor"* - Saemie Chouchane + + + +> *"one of the most naturally talented programmers you'll meet"* - Jamie Learmonth + +Available for **freelance** work in **Amsterdam** or remote.\ +:telephone_receiver: +31 203 699 866 [[call now]]\ +:envelope: robert@cambridge.me\ +:link: [linkedin.com/in/rcambrj]\ +:busts_in_silhouette: [Schedule an interview with me] + +## Experience + +### Frasers Group via Lab Digital, Utrecht [03/2024 - 10/2024] +* Maintained & developed `Typescript` backend running in microservices alongside `commercetools` +* Created & fixed `Terraform` infrastructure architecting the `AWS` `Lambda` & `ECS` services +* Introduced `nix` devshell to make onboarding & distribution of tooling easier + +### VanMoof via MA Micro Ltd, Amsterdam [10/2023 - 01/2024] +* Maintained & developed infrastructure managed with `Terraform`, inherited from previous team +* Brought up and improved engineer-facing `Kubernetes` clusters and staging environments +* Architected & executed plan to switch .com website to be reverse-proxied by `Cloudflare` +* Implemented `Cloudflare` rules & workers with `Terraform` to direct traffic to origins +* Assisted with the development of new internal services written in `Go` +* Wrote `Kubernetes` manifests to be instantiated by `ArgoCD` +* Continually scrutinised systems for security flaws and reported those detected + +### VanMoof, Amsterdam [07/2021 - 05/2023] +* Developed web services in `Go` (`golang` `Lambdas` via `Terraform`) +* Developed web services in `Serverless.js` (`node.js` `Lambdas` via `CloudFormation`) +* Brought up and maintained `AWS` services such as `RDS`, `DynamoDB`, `S3` and related network & ACL infrastructure +* Architected and developed a service to supply OTA firmware updates to bikes +* Architected and developed a service to track and investigate manufacturing faults + +### Polestar via code d'azur, Amsterdam [09/2020 - 07/2021] +* Mentored & guided a team of 12 +* Helped maintain the `gatsby` site with several hundred static pages running `React` with `emotion` +* Collaborated with other team representatives to prepare and complete architectural plans +* Investigated new integrations, contributing to formalisation of new visions into manageable pieces of work +* Maintained & improved the deployment pipelines in `Bitbucket` +* Architected system to run database migrations on an off-the-shelf headless CMS (`DatoCMS`) + +### PON Occasions via Dept, Amsterdam [01/2020 - 06/2020] +* Brought multi-deployment whitelabel frontend project from greenfield to live with a team of three +* Architected `Next.js` and vanilla `webpack` builds to use a common app codebase using `Typescript` +* Employed `React`, `styled-components` and `Storybook` for a scalable structure +* Analysed client requirements and translated into kanban-style work board +* Built a strong bridge of trust for expectations of delivery with client + +### [ABN AMRO Incubator] via Dept, Amsterdam [01/2020 - 02/2020] +* Implemented deployment of _multi-process_ `node.js` app to Heroku Pipelines +* Architected reverse proxy/frontend/backend apps as discrete Docker-like workspaces +* Created sane `node.js` `Typescript` configuration and linting +* Pioneered unit and end-to-end testing, integrated testing in the CI/CD build pipeline +* Collaborated with testers to write and improve automated `TestCafe` scripts +* Implemented `GraphQL` API to manage files on `box.com` with custom `Auth0` authentication/authorization + +### Etos via Dept, Amsterdam [09/2019 - 12/2019] +* Contributed new features and bugfixes to existing vanilla JS website +* Improved developer experience by installing sensible Javascript linting tools + +### Activia via AKQA, Amsterdam [05/2019 - 08/2019] +* Led the development of this global brand's new website construction +* Brought `Gatsby` project from greenfield to delivery with a team of two +* Architected `Contentful` data structure for a global rollout across many regions and languages +* Bootstrapped project with `React`, `styled-components`, `Storybook`, `jest` & `enzyme` +* Created smooth animated transitions between statically generated pages +* Setup and maintained deployment pipeline on `AWS Elastic Beanstalk` +* Initially solo, laid the ground work for the new Amsterdam engineering team to grow with best practices + +### Imperial Tobacco/Fontem (blu.com), Amsterdam [01/2019 - 04/2019] +* Configured `webpack` and `Babel` for `Jest`, `Storybook` and `Next.js` with `Typescript` +* Configured reverse proxy for micro-frontends with localisation and health checks +* Designed major parts of the API specification, with comprehensive buy in from the frontend+backend team +* Created new page components in `React` with `styled-components` and matching Contentful model architecture +* Improved `OpenAPI`/`Swagger` code generation templates for `Typescript` to better support inheritance/polymorphism + +### Ace & Tate, Amsterdam [10/2018 - 12/2018] +* Forged a plan to unobtrusively transition to continuous integration+deployment pipeline +* Dockerised services and deployed them to `AWS EB` via `CircleCI` and `AWS ECR` +* Identified data acquisition failures from third parties and built `node.js` caching layer to mitigate +* Created high impact interactions in `react-native` with careful attention to user experience cost +* Rebuilt parts of the `React` application to gain confidence by way of thorough unit testing +* Coached the team through agile principles, ultimately building trust in development capacity + +### Gain Theory via Tweag IO, London [03/2018 - 07/2018] +* Rebuilt client-facing data visualisation tools in `reactjs/redux` using `d3` and `recharts` +* Analysed client requirements to establish business data structure and functionality +* Engineered `node.js` service to transform data structure and store in `postgres` +* Implemented SVO-based ACL using `cancan` and `passportjs` +* Dockerised all services, plus `docker-compose` for developer experience + +### Wonderbill via Boston Consulting Group, London [07/2016 - 02/2018] +* Developed a web scraping framework to retrieve consumer billing data in a `Chrome` extension +* Architected data structure to store billing data supporting a wide range of account shapes +* Dockerised services to retrieve billing data programmatically with a `node.js` wrapper to invoke `Chrome` via `Selenium` +* Trained and led a small team of developers creating scripts to scrape & store billing data +* Architected and implemented strategies to track, follow and improve billing data accuracy +* Repeatedly exceeded improvement targets for data accuracy and acquisition efficiency +* Contributed small improvements to `golang` & `ruby` data storage services + +### NHS England via Web Technology Group, London [01/2016 - 06/2016] +* Developed a web-based CMS-like document management intranet site running `reactjs/redux` powered by `Nuxeo` + +### Arcadia Group, London [08/2015 - 12/2015] +* Developed leading UK retail brands' ecommerce front ends +* Created reusable logic across the 8 retail sites whilst supporting brand identity + +### Square Enix, London [03/2015 - 06/2015] +* Continued development game website in `backbone` and `marionette` +* Created an avatar provider microservice +* Iterated authentication library to support new major features retaining backwards compatibility + +### Expedia, London [12/2014 - 02/2015] +* Developed internal analytics/reporting intranet site running `backbone` +* Refactored non-standard `node.js` `express` API into organised RESTful endpoints +* Implemented MVP (presenter) architecture to abstract business logic from API URLs +* Rewrote the ACL to use permission-based authorisation with hierarchy and groups + +### Square Enix, London [05/2014 - 10/2014] +* Developed website for a game in `backbone` and `marionette` +* Created a cross-domain consumer authentication (SSO) library for company-wide roll out + +### Sportlobster, London [01/2014 - 03/2014] +* Led & mentored a team of 3 frontend developers using Javascript + +### Boxlight, London [10/2010 - 12/2013] +* Architected & developed hundreds of web applications in PHP and Javascript +* Deployed using rsync to AWS EC2 machines, eventually with Jenkins + +[call now]: https://rcambrj.github.io/resume/callme.html +[linkedin.com/in/rcambrj]: https://linkedin.com/in/rcambrj +[Schedule an interview with me]: https://calendly.com/rcambrj/interview \ No newline at end of file diff --git a/README.pdf b/README.pdf new file mode 100644 index 0000000..69e6ba2 Binary files /dev/null and b/README.pdf differ diff --git a/callme.html b/callme.html new file mode 100644 index 0000000..3a5ade1 --- /dev/null +++ b/callme.html @@ -0,0 +1,26 @@ + +
+ +
+ + \ No newline at end of file diff --git a/devshell.toml b/devshell.toml new file mode 100644 index 0000000..0088625 --- /dev/null +++ b/devshell.toml @@ -0,0 +1,3 @@ +# https://numtide.github.io/devshell +[[commands]] +package = "go" diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..77aef10 --- /dev/null +++ b/flake.lock @@ -0,0 +1,115 @@ +{ + "nodes": { + "devshell": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "systems": "systems" + }, + "locked": { + "lastModified": 1692793255, + "narHash": "sha256-yVyj0AE280JkccDHuG1XO9oGxN6bW8ksr/xttXcXzK0=", + "owner": "numtide", + "repo": "devshell", + "rev": "2aa26972b951bc05c3632d4e5ae683cb6771a7c6", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "devshell", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1692799911, + "narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1729256560, + "narHash": "sha256-/uilDXvCIEs3C9l73JTACm4quuHUsIHcns1c+cHUJwA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "4c2fcb090b1f3e5b47eaa7bd33913b574a11e0a0", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devshell": "devshell", + "flake-compat": "flake-compat", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..0d77f6f --- /dev/null +++ b/flake.nix @@ -0,0 +1,34 @@ +{ + description = "nix devshell for rcambrj/resume"; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable"; + inputs.devshell.url = "github:numtide/devshell"; + inputs.devshell.inputs.nixpkgs.follows = "nixpkgs"; + inputs.flake-utils.url = "github:numtide/flake-utils"; + inputs.flake-utils.inputs.nixpkgs.follows = "nixpkgs"; + + inputs.flake-compat = { + url = "github:edolstra/flake-compat"; + flake = false; + }; + + outputs = + { + self, + flake-utils, + devshell, + nixpkgs, + ... + }: + flake-utils.lib.eachDefaultSystem (system: { + devShells.default = + let + pkgs = import nixpkgs { + inherit system; + + overlays = [ devshell.overlays.default ]; + }; + in + pkgs.devshell.mkShell { imports = [ (pkgs.devshell.importTOML ./devshell.toml) ]; }; + }); +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5c16a6a --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module github.com/rcambrj/resume + +go 1.23.2 + +require github.com/go-rod/rod v0.116.2 + +require ( + github.com/ysmood/fetchup v0.2.4 // indirect + github.com/ysmood/goob v0.4.0 // indirect + github.com/ysmood/got v0.40.0 // indirect + github.com/ysmood/gson v0.7.3 // indirect + github.com/ysmood/leakless v0.9.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2bc6141 --- /dev/null +++ b/go.sum @@ -0,0 +1,16 @@ +github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA= +github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg= +github.com/ysmood/fetchup v0.2.4 h1:2kfWr/UrdiHg4KYRrxL2Jcrqx4DZYD+OtWu7WPBZl5o= +github.com/ysmood/fetchup v0.2.4/go.mod h1:hbysoq65PXL0NQeNzUczNYIKpwpkwFL4LXMDEvIQq9A= +github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= +github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= +github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg= +github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk= +github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q= +github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg= +github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY= +github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= +github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= +github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= +github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU= +github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= diff --git a/index.html b/index.html new file mode 100644 index 0000000..a102aa6 --- /dev/null +++ b/index.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..2146410 --- /dev/null +++ b/main.go @@ -0,0 +1,150 @@ +package main + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "os" + + "github.com/go-rod/rod" + "github.com/go-rod/rod/lib/proto" + "github.com/go-rod/rod/lib/utils" +) + +type GithubMarkdownBody struct { + Text string `json:"text"` + Mode string `json:"mode"` + Context string `json:"context"` +} + +func getMarkdown() ([]byte, error) { + md, err := os.ReadFile("README.md") + if err != nil { + return nil, fmt.Errorf("unable to open file: %w", err) + } + return md, nil +} +func getMarkup(md []byte) (*string, error) { + reqBody := GithubMarkdownBody{ + Text: string(md), + Mode: "gfm", + Context: "rcambrj/resume", + } + reqBodyBytes, err := json.Marshal(reqBody) + client := &http.Client{} + req, err := http.NewRequest("POST", "https://api.github.com/markdown", bytes.NewBuffer(reqBodyBytes)) + if err != nil { + return nil, fmt.Errorf("unable to create http request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + res, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("unable to fetch markdown from github: %w", err) + } + defer res.Body.Close() + resBodyBytes, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("unable to read body of http response: %w", err) + } + resBodyString := string(resBodyBytes) + if res.StatusCode != http.StatusOK { + return nil, errors.New(fmt.Sprintf("unexpected status code fetching from github: %d %s", res.StatusCode, resBodyString)) + } + return &resBodyString, nil +} + +func getGithubCSS() (*string, error) { + res, err := http.Get("https://raw.githubusercontent.com/sindresorhus/github-markdown-css/main/github-markdown.css") + if err != nil { + return nil, fmt.Errorf("unable to fetch github css: %w", err) + } + defer res.Body.Close() + resBodyBytes, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("unable to read body of http response: %w", err) + } + resBodyString := string(resBodyBytes) + return &resBodyString, nil +} + +func wrapHTML(markup string, css string) string { + return fmt.Sprintf( + ` + + +
%s
+ `, + css, + markup) +} + +func writeHTML(html string) error { + err := os.WriteFile("README.html", []byte(html), 0644) + if err != nil { + return fmt.Errorf("unable to write html file: %w", err) + } + return nil +} + +func writePDF(html string) error { + page := rod.New().MustConnect().MustPage().MustSetDocumentContent(html).MustWaitLoad() + pdf, err := page.PDF(&proto.PagePrintToPDF{}) + if err != nil { + return err + } + err = utils.OutputFile("README.pdf", pdf) + return nil +} + +func main() { + log.Print("Opening file...") + markdown, err := getMarkdown() + if err != nil { + log.Fatal(err) + } + log.Print("Fetching Github CSS...") + css, err := getGithubCSS() + if err != nil { + log.Fatal(err) + } + log.Print("Converting markdown to HTML...") + markup, err := getMarkup(markdown) + if err != nil { + log.Fatal(err) + } + html := wrapHTML(*markup, *css) + log.Print("Writing HTML to file...") + err = writeHTML(html) + if err != nil { + log.Fatal(err) + } + log.Print("Writing PDF to file...") + err = writePDF(html) + if err != nil { + log.Fatal(err) + } +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..d7bad49 --- /dev/null +++ b/shell.nix @@ -0,0 +1,18 @@ +# Use `builtins.getFlake` if available +if builtins ? getFlake then + let + scheme = if builtins.pathExists ./.git then "git+file" else "path"; + in + (builtins.getFlake "${scheme}://${toString ./.}").devShells.${builtins.currentSystem}.default + +# Otherwise we'll use the flake-compat shim +else + (import ( + let + lock = builtins.fromJSON (builtins.readFile ./flake.lock); + in + fetchTarball { + url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; + sha256 = lock.nodes.flake-compat.locked.narHash; + } + ) { src = ./.; }).shellNix