feat: Add syntax highlights and RSS feed

This commit is contained in:
Robert Prehn 2023-09-24 07:52:16 +00:00
parent caa1e26a93
commit 46c23c5c28
No known key found for this signature in database
24 changed files with 238 additions and 84 deletions

View file

@ -1,4 +1,6 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
import_deps: [:phoenix_live_view],
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
locals_without_parens: [config: ]
]

View file

@ -22,10 +22,6 @@ a {
@apply text-blue;
}
a::before {
/* content: "⇲"; */
}
nav a {
@apply text-dark;
}
@ -41,14 +37,62 @@ p {
margin-bottom: var(--vspace);
}
a {
display: inline-block;
}
a:hover {
@apply bg-light text-dark;
}
ul li {
list-style-type: "* ";
margin-left: 3ch;
margin-bottom: var(--vspace);
}
ol li {
list-style-type: decimal;
margin-left: 3ch;
margin-bottom: var(--vspace);
}
h1 {
@apply bg-red text-dark uppercase text-center;
margin-bottom: var(--vspace);
}
h2 {
@apply font-bold bg-green;
margin-top: calc(var(--vspace) * 2);
margin-bottom: var(--vspace);
}
/* Code Highlighting */
code {
@apply block;
margin-bottom: var(--vspace);
}
code.inline {
@apply bg-blue text-dark;
display: inline;
}
.line-number {
@apply align-top text-blue;
opacity: 0.75;
padding-right: 1ch;
user-select: none;
-khtml-user-select: none;
-o-user-select: none;
-moz-user-select: -moz-none;
-webkit-user-select: none;
}
.line {
white-space: pre-wrap;
}
/* Web Fonts */
@font-face {
font-family: "JetBrainsMono";
src: url("/fonts/JetBrainsMono-VariableFont_wght.ttf");

View file

@ -20,6 +20,6 @@ config :esbuild,
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
]
config :tree_sitter, :version, "0.20.8"
config :tree_sitter, version: "0.20.8", config_directory: "tree-sitter"
config :pre_dot_hn, host: "pre.hn"
config :pre_dot_hn, host: "pre.hn", base: "https://pre.hn/"

View file

@ -55,6 +55,7 @@ defmodule PreDotHn do
|> Enum.sort_by(&Map.get(&1, "date"), :desc)
|> Enum.map(&atomize_keys/1)
make_feed(posts)
index = make_index(posts)
Enum.map([index | posts], &write_page/1)
@ -71,14 +72,12 @@ defmodule PreDotHn do
body =
~H"""
<ul>
<%= for post <- @posts do %>
<li style="margin-bottom: 1rem">
<span class="text-yellow align-top"><%= post.date %></span>&nbsp;
<a class="text-ellipsis overflow-hidden" style="max-width: 40ch" href={"/#{post.slug}/"}><%= post.title %></a>
</li>
<% end %>
</ul>
<%= for post <- @posts do %>
<div style="margin-bottom: 1rem">
<span class="text-yellow align-top"><%= post.date %></span>&nbsp;
<a class="text-ellipsis overflow-hidden inline-block" style="max-width: 40ch" href={"/#{post.slug}/"}><%= post.title %></a>
</div>
<% end %>
"""
|> rendered_to_string()
@ -90,6 +89,73 @@ defmodule PreDotHn do
}
end
def make_feed(posts) do
posts = Enum.take(posts, 10)
base = Application.get_env(:pre_dot_hn, :base, "/")
assigns = %{base: base, posts: posts}
body =
EEx.eval_string("""
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>pre.hn</title>
<description>Robert Prehn's personal blog.</description>
<link>https://pre.hn</link>
<atom:link href="https://pre.hn/feed.rss" rel="self" type="application/rss+xml" />
<%= for post <- @posts do %>
<item>
<title><%= HtmlSanitizeEx.strip_tags(post.title) %></title>
<description>
<%= Phoenix.HTML.html_escape(post.body) |> elem(1) %>
</description>
<pubDate><%= rss_date_format.(post.date) %></pubDate>
<guid isPermaLink="true"><%= @base %><%= post.slug %>/</guid>
</item>
<% end %>
</channel>
</rss>
""", assigns: assigns, rss_date_format: &rss_date_format/1)
path = Path.join(["priv", "static", "feed.rss"])
File.write!(path, body)
end
@days_of_week %{
1 => "Mon",
2 => "Tue",
3 => "Wed",
4 => "Thu",
5 => "Fri",
6 => "Sat",
7 => "Sun"
}
@months %{
1 => "Jan",
2 => "Feb",
3 => "Mar",
4 => "Apr",
5 => "May",
6 => "Jun",
7 => "Jul",
8 => "Aug",
9 => "Sep",
10 => "Oct",
11 => "Nov",
12 => "Dec"
}
def rss_date_format(date_string) do
date = Date.from_iso8601!(date_string)
day = Map.get(@days_of_week, Date.day_of_week(date))
month = Map.get(@months, date.month)
"#{day}, #{date.day} #{month} #{date.year} 00:00:00 -0600"
end
attr(:title, :string, required: true)
attr(:description, :string)
slot(:inner_block, required: true)
@ -147,6 +213,7 @@ defmodule PreDotHn do
<.layout title={@title} description={assigns[:description]}>
<main class="container mx-auto">
<section class="box border border-light">
<h1><%= @title %></h1>
<%= {:safe, @body} %>
</section>
</main>

View file

@ -11,6 +11,7 @@ defmodule PreDotHn.Markdown do
{:ok, ast, _} ->
ast
|> Earmark.Transform.map_ast(&transformer/1)
|> walk()
|> Enum.map(&maybe_remove_para(&1, inner_html))
other ->
@ -37,6 +38,18 @@ defmodule PreDotHn.Markdown do
defp transformer(other), do: other
def walk(list) when is_list(list), do: Enum.map(list, &walk/1)
def walk(binary) when is_binary(binary), do: binary
def walk({"pre", _attrs, [{"code", attrs, [children], _}], _meta}) do
lang = Earmark.AstTools.find_att_in_node(attrs, "class")
walk(highlight(children, lang))
end
def walk({tag, attrs, children, meta}), do: {tag, attrs, walk(children), meta}
defp maybe_remove_para(node, false), do: node
defp maybe_remove_para({"p", _attrs, children, _meta}, true),
@ -44,6 +57,28 @@ defmodule PreDotHn.Markdown do
defp maybe_remove_para(other, true), do: other
defp highlight([text], lang), do: highlight(text, lang)
defp highlight(text, lang) do
{:ok, highlight} = TreeSitter.highlight_html(text, lang || "plain")
[_preamble, rest] = String.split(highlight, "<table>")
[tbody, _rest] = String.split(rest, "</table>")
table = "<table>\n#{tbody}\n</table>"
{"code", [], [table], %{verbatim: true}}
end
def floki2earmark(list) when is_list(list) do
Enum.map(list, &floki2earmark/1)
end
def floki2earmark(text) when is_binary(text), do: text
def floki2earmark({tag, attrs, children}), do: {tag, attrs, floki2earmark(children), %{}}
defp add_trailing_newline(string) when is_binary(string), do: "#{string}\n"
defp add_trailing_newline(other), do: other

View file

@ -29,7 +29,10 @@ defmodule PreDotHn.MixProject do
{:rustler, "~> 0.29.1"},
{:tree_sitter, path: "../tree_sitter"},
{:earmark_parser, "~> 1.4"},
{:earmark, "~> 1.4"}
{:earmark, "~> 1.4"},
{:floki, "~> 0.34.3"},
{:html_sanitize_ex, "~> 1.4"},
{:phoenix_html, "~> 3.3.2"}
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]

View file

@ -3,8 +3,11 @@
"earmark": {:hex, :earmark, "1.4.43", "2024a0e9fe9bd5ef78fb9c87517de6c6d7deaf1cffdf6572fac3dd49cb34c433", [:mix], [], "hexpm", "958011ea938bc4018797bda3f8d0c871ab04621785bedc1e7188fb079dea2f5b"},
"earmark_parser": {:hex, :earmark_parser, "1.4.35", "437773ca9384edf69830e26e9e7b2e0d22d2596c4a6b17094a3b29f01ea65bb8", [:mix], [], "hexpm", "8652ba3cb85608d0d7aa2d21b45c6fad4ddc9a1f9a1f1b30ca3a246f0acc33f6"},
"esbuild": {:hex, :esbuild, "0.7.1", "fa0947e8c3c3c2f86c9bf7e791a0a385007ccd42b86885e8e893bdb6631f5169", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "66661cdf70b1378ee4dc16573fcee67750b59761b2605a0207c267ab9d19f13c"},
"floki": {:hex, :floki, "0.34.3", "5e2dcaec5d7c228ce5b1d3501502e308b2d79eb655e4191751a1fe491c37feac", [:mix], [], "hexpm", "9577440eea5b97924b4bf3c7ea55f7b8b6dce589f9b28b096cc294a8dc342341"},
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.4.3", "67b3d9fa8691b727317e0cc96b9b3093be00ee45419ffb221cdeee88e75d1360", [:mix], [{:mochiweb, "~> 2.15 or ~> 3.1", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "87748d3c4afe949c7c6eb7150c958c2bcba43fc5b2a02686af30e636b74bccb7"},
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"mochiweb": {:hex, :mochiweb, "3.2.1", "ff287e1ec653a0828f226cd5a009d52be74537dc3fc274b765525a77ce01f8ec", [:rebar3], [], "hexpm", "975466d335403a78cd58186636b8e960e3c84c4d9c1a85eb7fe53b6a5dd54de7"},
"phoenix": {:hex, :phoenix, "1.7.7", "4cc501d4d823015007ba3cdd9c41ecaaf2ffb619d6fb283199fa8ddba89191e0", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "8966e15c395e5e37591b6ed0bd2ae7f48e961f0f60ac4c733f9566b519453085"},
"phoenix_html": {:hex, :phoenix_html, "3.3.2", "d6ce982c6d8247d2fc0defe625255c721fb8d5f1942c5ac051f6177bffa5973f", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "44adaf8e667c1c20fb9d284b6b0fa8dc7946ce29e81ce621860aa7e96de9a11d"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.19.5", "6e730595e8e9b8c5da230a814e557768828fd8dfeeb90377d2d8dbb52d4ec00a", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b2eaa0dd3cfb9bd7fb949b88217df9f25aed915e986a28ad5c8a0d054e7ca9d3"},

View file

@ -1,6 +1,6 @@
---
date: 2018-12-06
title: About Robert Prehn
title: About Me
slug: index
---
@ -11,6 +11,5 @@ I'm a software engineering leader and independent consultant. You can hire me to
A better future is possible!
<p class="h-card">
<img alt="Photo of Robert Prehn" class="Gravatar u-photo" src="https://www.gravatar.com/avatar/925bde4cfd5d42530618204b73200d52">
<a class="p-name u-url" href="https://pre.hn" target="_blank">Robert Prehn</a> | <a class="u-key" href="https://keybase.io/prehnra/pgp_keys.asc" target="_blank">Public Key</a> | <a href="https://github.com/prehnRA" rel="me" target="_blank">GitHub</a> | <a href="https://keybase.io/prehnra" rel="me" target="_blank">Keybase</a> | <a href="https://mastodon.social/@prehnra" rel="me" target="_blank">Fediverse</a>
</p>

View file

@ -2,7 +2,7 @@
title: Minimal Phoenix and Elixir Dockerfile Example
---
Recently, I was setting up a Dockerfile for a Phoenix web app. There are official **Elixir** Docker images, but finding one specifically set up to handle the Phoenix Framework with asset compilation is messier territory. There's no single set of \"official\" configurations.
Recently, I was setting up a Dockerfile for a Phoenix web app. There are official **Elixir** Docker images, but finding one specifically set up to handle the Phoenix Framework with asset compilation is messier territory. There's no single set of "official" configurations.
Everybody has an opinion about how you should do this. A lot of these opinions involve adding a lot of bells and whistles. For one, their preferred solution might include Docker Compose. For another, Distillery releases. Since I had not successfully deployed my app with Docker yet at all, I wanted fewer variables to debug.
@ -36,7 +36,7 @@ RUN mix deps.compile
RUN mix compile
RUN mix phx.digest
CMD [\"mix\", \"phx.server\"]
CMD ["mix", "phx.server"]
```
This starts with the elixir alpine image. Alpine is a skinny Linux that works well in containers. I've found that it is a suitable base for Elixir apps. In my case, I needed a C toolchain to compile some libraries. You might not need that part. Then it sets up hex and rebar for fetching and building dependencies. Then it adds the application directory. It sets the default port and environment. It fetches the dependencies, compiles them, compiles the app, and digests the assets. Then, it starts the server. That's it.

View file

@ -4,11 +4,11 @@ title: Setting up CI/CD for Docker and Kubernetes Using Drone
I have been a Travis CI user. However, Travis has gotten less reliable for me lately. On top of that, I have qualms about how the acquisition of Travis by Idera, and the subsequent layoffs, were handled. Travis is also a square peg in the octagonal hole of my Kubernetes environment. It is a hosted, external service. Everything else I use to develop my applications is hosted inside of my cluster. My Docker registry and my gitops operator run in my cluster. My databases are in my cluster. My storage provider and object store run in cluster. My apps run in cluster. Why would I run my CI/CD service outside of the cluster?
I came across Drone in my research about alternatives. Drone is a fully container-native, container-loving CI solution. It's Docker all the way down. Since my application is already \"Dockerified\", my hosting environment is all Docker all day, and my deployments are already in the form of a Docker push, why not do CI/CD in Docker as well?
I came across Drone in my research about alternatives. Drone is a fully container-native, container-loving CI solution. It's Docker all the way down. Since my application is already "Dockerified", my hosting environment is all Docker all day, and my deployments are already in the form of a Docker push, why not do CI/CD in Docker as well?
Drone is basically a small framework for running CI jobs made of docker containers. You build your pipeline as a series of steps, each of which is a docker base image, some configuration, and your test commands. Drone also has all the standard integrations you would expect for a CI service-- it talks to GitHub, GitLab, Bitbucket, and more.
To use Drone, you'll have to embrace the Docker way. Let go of your test scripts that are building everything from a ubuntu image or a language version manager. Let go of the \"special case magic\" way DBs and other supporting services are handled in other CI platforms. In exchange, you'll find that Drone will let you use any language, any tools, and any languages, so long as they have a Docker image.
To use Drone, you'll have to embrace the Docker way. Let go of your test scripts that are building everything from a ubuntu image or a language version manager. Let go of the "special case magic" way DBs and other supporting services are handled in other CI platforms. In exchange, you'll find that Drone will let you use any language, any tools, and any languages, so long as they have a Docker image.
## Drone Setup on Kubernetes
@ -16,7 +16,7 @@ I followed [this official (but experimental) guide](https://docs.drone.io/instal
Here's the configuration I used for Kubernetes:
```yaml
```
---
apiVersion: v1
kind: Namespace
@ -44,23 +44,23 @@ spec:
image: drone/drone:1.0.0
env:
- name: DRONE_KUBERNETES_ENABLED
value: \"true\"
value: "true"
- name: DRONE_KUBERNETES_NAMESPACE
value: \"drone\"
value: "drone"
- name: DRONE_GITHUB_SERVER
value: \"https://github.com\"
value: "https://github.com"
- name: DRONE_GITHUB_CLIENT_ID
value: \"REDACTED\"
value: "REDACTED"
- name: DRONE_GITHUB_CLIENT_SECRET
value: \"REDACTED\"
value: "REDACTED"
- name: DRONE_RPC_SECRET
value: \"REDACTED\"
value: "REDACTED"
- name: DRONE_SERVER_HOST
value: \"REDACTED.example.com\"
value: "REDACTED.example.com"
- name: DRONE_SERVER_PROTO
value: \"https\"
value: "https"
- name: DRONE_USER_FILTER
value: \"prehnRA\"
value: "prehnRA"
- name: DRONE_USER_CREATE
value: username:prehnRA,admin:true
- name: DRONE_DATABASE_DRIVER
@ -95,11 +95,11 @@ apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
ingress.kubernetes.io/ssl-redirect: \"true\"
nginx.ingress.kubernetes.io/ssl-redirect: \"true\"
kubernetes.io/tls-acme: \"true\" # enable certificates
ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
kubernetes.io/tls-acme: "true" # enable certificates
certmanager.k8s.io/cluster-issuer: letsencrypt
kubernetes.io/ingress.class: \"nginx\"
kubernetes.io/ingress.class: "nginx"
labels:
app: drone
name: drone
@ -124,10 +124,10 @@ metadata:
name: drone-postgres
namespace: drone
spec:
version: \"10.2-v1\"
version: "10.2-v1"
storageType: Durable
storage:
storageClassName: \"rook-block\"
storageClassName: "rook-block"
accessModes:
- ReadWriteOnce
resources:
@ -149,7 +149,7 @@ kubectl create secret generic drone-postgres-url -n drone --from-file=./url
The application I wanted to test and deploy through Drone is an Elixir app that is released by semantic-release as a Docker image. I've written previously [on the Revelry blog](https://revelry.co/semantic-release/) about how to do that.
You configure Drone using a .drone.yml file. The main YAML object defined in that file is a \"pipeline\", which defines a series of steps and services which Drone will use to run your tests and deploy your app.
You configure Drone using a .drone.yml file. The main YAML object defined in that file is a "pipeline", which defines a series of steps and services which Drone will use to run your tests and deploy your app.
A basic pipeline for my app might look like this:
```
@ -189,10 +189,10 @@ services:
ports:
- 3306
environment:
MYSQL_DATABASE: \"cms_test\"
MYSQL_USER: \"REDACTED\"
MYSQL_PASSWORD: \"REDACTED\"
MYSQL_RANDOM_ROOT_PASSWORD: \"yes\"
MYSQL_DATABASE: "cms_test"
MYSQL_USER: "REDACTED"
MYSQL_PASSWORD: "REDACTED"
MYSQL_RANDOM_ROOT_PASSWORD: "yes"
```
@ -200,11 +200,11 @@ I also configured my test suite to use a mariadb database at the host `cms-datab
My tests pass!
Next, I need to add my deployment step. As I mentioned before, this app deploys as a docker image, via semantic-release and semantic-release-docker. In order to do that, I need to use a \"docker in docker\" image-- which is just what it says, a Docker image containing the Docker daemon and Docker CLI.
Next, I need to add my deployment step. As I mentioned before, this app deploys as a docker image, via semantic-release and semantic-release-docker. In order to do that, I need to use a "docker in docker" image-- which is just what it says, a Docker image containing the Docker daemon and Docker CLI.
It's actually best to use two of these. Drone (and Docker) prefer if any long running services, such as the Docker daemon, run in their own containers. In my experience, trying to run the Docker daemon in the background of a container that is also doing other commands overcomplicates things. Running the Docker daemon in an isolated container works better, because Docker has provided out of the box initialization scripts for that scenario.
Here's the pipeline with both \"dind\" parts added:
Here's the pipeline with both "dind" parts added:
```
---
@ -243,10 +243,10 @@ services:
ports:
- 3306
environment:
MYSQL_DATABASE: \"cms_test\"
MYSQL_USER: \"REDACTED\"
MYSQL_PASSWORD: \"REDACTED\"
MYSQL_RANDOM_ROOT_PASSWORD: \"yes\"
MYSQL_DATABASE: "cms_test"
MYSQL_USER: "REDACTED"
MYSQL_PASSWORD: "REDACTED"
MYSQL_RANDOM_ROOT_PASSWORD: "yes"
- name: docker
image: docker:dind
privileged: true
@ -264,7 +264,7 @@ volumes:
That's a lot of new pieces. What's going on? Well, we've added a `docker:dind` step to the pipeline. This will run our Docker commands, for building and pushing the Docker image. We also need npm and NodeJS, because semantic-release is a Node package. We install npm and update it to the latest version for good measure. Then `(cd assets; npm install; npm run deploy)` builds my assets for production. `npm install` installs semantic-release and the various plugins I use (see my article from the Revelry blog). I build the Docker image. Then, I run semantic-release. Since I'm using semantic-release-docker, it will login to my Docker repo and push the image.
In order to actually build the image, we need a running Docker daemon. That's where the second `dind` comes in. This one is a service. It runs in privileged mode (required for the dind daemon), which means my Drone project must be flagged as \"trusted.\" We use a shared volume to allow the Docker CLI running in my deployment step to communicate with the Docker daemon running as a service.
In order to actually build the image, we need a running Docker daemon. That's where the second `dind` comes in. This one is a service. It runs in privileged mode (required for the dind daemon), which means my Drone project must be flagged as "trusted." We use a shared volume to allow the Docker CLI running in my deployment step to communicate with the Docker daemon running as a service.
I need to provide some credentials for GitHub and for my Docker registry. I do that by exposing them as environment variables. Since I don't want to check in a sensitive credential to git, I use Drone secrets for passwords and tokens. You can set the value of the secret in the project settings UI.
@ -282,7 +282,7 @@ When you put it all together, Drone will do the following sequence:
## Other Drone Features
I had a lightbulb moment while I was working with Drone. I had been wondering things like \"I wonder which DBs Drone supports\" or \"I wonder which languages Drone supports.\" Then I realized: Drone supports everything that has a Docker image. To me, this makes it a more flexible and powerful tool than a CI architected like Travis or Codeship. In those CIs, you either have to wait for official support for your language (or service), or you have to hack together a working test script from a base intended for a different language or DB. The way Drone does this means that Drone immediately has a \"feature list\" longer than I can write here.
I had a lightbulb moment while I was working with Drone. I had been wondering things like "I wonder which DBs Drone supports" or "I wonder which languages Drone supports." Then I realized: Drone supports everything that has a Docker image. To me, this makes it a more flexible and powerful tool than a CI architected like Travis or Codeship. In those CIs, you either have to wait for official support for your language (or service), or you have to hack together a working test script from a base intended for a different language or DB. The way Drone does this means that Drone immediately has a "feature list" longer than I can write here.
Beyond this, there are other Drone features worth mentioning:

View file

@ -11,7 +11,7 @@ slug: this-weekend-i-read-2019-04-14
> Between 2015 and 2018, OSHA reported 41 “severe” injuries resulting in hospitalization, including six amputations and 15 fractures, associated with Amazon delivery or fulfillment jobs. 
Amazon workers are receiving severe, life-changing injuries on the job, and Amazon is covering it up using a system of in-house \"clinics\", complicit company-mandated doctors, and missing OSHA filings.
Amazon workers are receiving severe, life-changing injuries on the job, and Amazon is covering it up using a system of in-house "clinics", complicit company-mandated doctors, and missing OSHA filings.
Who doesn't think Amazon workers suffering through this should have a union? Well, Amazon for one. The company is using old-school union-busting tactics to single out and remove pro-union workers.
@ -19,7 +19,7 @@ Who doesn't think Amazon workers suffering through this should have a union? Wel
- [Amazon and Union at Odds Over Firing of Staten Island Warehouse Worker](https://www.nytimes.com/2019/03/20/business/economy/amazon-warehouse-labor.html) by Noam Scheiber at NYT
- [Amazon lobbies to exempt employees from labor protections](https://apnews.com/5c01ffdd9fbb48639fc43bc376f501e4) by Tom Janes at AP
- [The Relentless Misery of Working Inside an Amazon Warehouse](https://onezero.medium.com/relentless-com-life-as-a-cog-in-amazons-e-tail-machine-d46b3ef05eb8) by Cameron Brady-Turner at Medium
- [Colony of Hell: 911 Calls From Inside Amazon Warehouses](https://www.thedailybeast.com/amazon-the-shocking-911-calls-from-inside-its-warehouses?ref=home) by Max Zahn, Sharif Paget at The Daily Beast (\"Warning: This story addresses suicidal threats by Amazon employees.\")
- [Colony of Hell: 911 Calls From Inside Amazon Warehouses](https://www.thedailybeast.com/amazon-the-shocking-911-calls-from-inside-its-warehouses?ref=home) by Max Zahn, Sharif Paget at The Daily Beast ("Warning: This story addresses suicidal threats by Amazon employees.")
# Meritocracy is Still Fake
@ -42,6 +42,6 @@ While I don't agree with the authors entirely, [this article](https://www.fastco
Where their argument falters is that they seem to assume that which of these models will emerge is a function of which one best addresses the challenges of the modern era, climate change, and the pressures of automation. That's not how economies change. Economies change as a function of who holds economic power, those people's interests, and how people generally relate to economic activity. Without radical economic democracy which places power in many hands, whatever comes next will only serve the few who currently hold power.
Though I do have to give a special shout-out for introducing me to the term \"doughnut economics.\"
Though I do have to give a special shout-out for introducing me to the term "doughnut economics."
- [Dont Be Scared About the End of Capitalism—Be Excited to Build What Comes Next](https://www.fastcompany.com/40454254/dont-be-scared-about-the-end-of-capitalism-be-excited-to-build-what-comes-next) by Jason Hickel and Martin Kirk at Fast Company

View file

@ -13,7 +13,7 @@ Heroku supports Docker via the [Container Registry and Runtime](https://devcente
4. Build the image and push it to the Heroku registry: `heroku container:push web`.
5. Deploy it to your dynos: `heroku container:release web`.
6. Check your site and make sure everything works.
7. If your application does not boot and logs the error message `Shall I install Hex? (if running non-interactively, use \"mix local.hex --force\") [Yn] ** (Mix) Could not find an SCM for dependency :phoenix from <app_name>.Mixfile`, you may have to make a small change to your Dockerfile.
7. If your application does not boot and logs the error message `Shall I install Hex? (if running non-interactively, use "mix local.hex --force") [Yn] ** (Mix) Could not find an SCM for dependency :phoenix from <app_name>.Mixfile`, you may have to make a small change to your Dockerfile.
- Add ENV MIX_HOME=/opt/mix before you install hex & rebar in your Dockerfile. The reason this is needed from what I can gather is that Herokus Docker execution environment is different from your local development environment.
For reference, heres my full Dockerfile for my Phoenix application:
@ -45,5 +45,5 @@ RUN mix deps.compile
RUN MAKE=cmake mix compile
RUN mix phx.digest
CMD [\"mix\", \"phx.server\"]
CMD ["mix", "phx.server"]
```

View file

@ -12,7 +12,7 @@ Visible bread lines happen when a society tries to feed everyone, but can't. Peo
Capitalism has bread lines. They are invisible and omnipresent. You don't see capitalist bread lines because everyone knows that society will make no effort to feed you unless you have money to pay. Why don't people line up for bread at American groceries? Not because they aren't hungry, but because they know it is useless. If you are going to be hungry either way, you might as well save the gas money.
Occasionally, bread lines are allowed to poke through into visibility, so long as they maintain a suitable aesthetic. What are food banks and homeless shelters other than _our bread lines_? Both are evidence of needs unmet. We just consider it acceptable under the guise of \"charity,\" because charity convinces us that the foundation isn't rotten. \"Some people just fall through the cracks, but it mostly works,\" we reassure ourselves.
Occasionally, bread lines are allowed to poke through into visibility, so long as they maintain a suitable aesthetic. What are food banks and homeless shelters other than _our bread lines_? Both are evidence of needs unmet. We just consider it acceptable under the guise of "charity," because charity convinces us that the foundation isn't rotten. "Some people just fall through the cracks, but it mostly works," we reassure ourselves.
[37 million Americans don't reliably get enough to eat, including 11 million children.](https://hungerandhealth.feedingamerica.org/understand-food-insecurity/) That's over ten percent of us.

View file

@ -6,11 +6,11 @@ Since COVID-19 is a serious concern, a lot of companies are asking their employe
You aren't going to feel productive. And it isn't because you are distracted. In fact, it might be because you have fewer distractions!
Allow me to explain. If you are used to working in an office, you are probably accustomed to office chit-chat and doing little laps around the office all day. You talk to the person at the front desk, then to your friend two cubicles down. You get some coffee before you \"dive in.\" Your manager stops by to see how softball went. Someone stops by to say that it is Sylvia's birthday and there is cake in the break room. The guy from facilities needs to stand on your desk to fix that air vent. While he's at it you can't work anyway, so you chat about the Saints. And then you go get that cake, and a coffee refill. You sit down to work, but you stand up again one minute later after you print out that report. You walk to the printer, but your manager catches you on the way and wants to know what you think about the new format for the weekly business unit review. It's getting close to lunch time. &c, &c.
Allow me to explain. If you are used to working in an office, you are probably accustomed to office chit-chat and doing little laps around the office all day. You talk to the person at the front desk, then to your friend two cubicles down. You get some coffee before you "dive in." Your manager stops by to see how softball went. Someone stops by to say that it is Sylvia's birthday and there is cake in the break room. The guy from facilities needs to stand on your desk to fix that air vent. While he's at it you can't work anyway, so you chat about the Saints. And then you go get that cake, and a coffee refill. You sit down to work, but you stand up again one minute later after you print out that report. You walk to the printer, but your manager catches you on the way and wants to know what you think about the new format for the weekly business unit review. It's getting close to lunch time. &c, &c.
When you work from home your day is different. You are probably trying extra hard to be diligent. You make coffee before your designated start time, because you don't want to be away from your computer if your manager \"pings\" you. There is no one to chat with on your commute down the hall to your post in the spare bedroom. You might not even have a printer, but if you do, you can reach it from your chair. Your manager doesn't swing by your desk or catch you in the corridor. If they need to talk, they'll schedule a meeting. You eat lunch on a schedule and keep snacks on your desk.
When you work from home your day is different. You are probably trying extra hard to be diligent. You make coffee before your designated start time, because you don't want to be away from your computer if your manager "pings" you. There is no one to chat with on your commute down the hall to your post in the spare bedroom. You might not even have a printer, but if you do, you can reach it from your chair. Your manager doesn't swing by your desk or catch you in the corridor. If they need to talk, they'll schedule a meeting. You eat lunch on a schedule and keep snacks on your desk.
The result of this is a lot less \"fluff\" in your day. Because you are moving less and talking less, you feel like you are less productive. The truth is, you've probably been getting all your work done in [the three productive hours you actually have every day](https://www.inc.com/melanie-curtin/in-an-8-hour-day-the-average-worker-is-productive-for-this-many-hours.html). I'm not saying this is a bad thing! A lot of people have this revelation after some time working from home and they feel intense guilt about it. I'm telling you it is natural! [Medieval peasants worked about half as much as we do now!](https://groups.csail.mit.edu/mac/users/rauch/worktime/hours_workweek.html). [Hunter-gatherers work about half as much as typical office workers!](http://rewild.com/in-depth/leisure.html) I think we're honestly not built for eight hours of constantly coding or fiddling with reports in a day. We've been fooling ourselves.
The result of this is a lot less "fluff" in your day. Because you are moving less and talking less, you feel like you are less productive. The truth is, you've probably been getting all your work done in [the three productive hours you actually have every day](https://www.inc.com/melanie-curtin/in-an-8-hour-day-the-average-worker-is-productive-for-this-many-hours.html). I'm not saying this is a bad thing! A lot of people have this revelation after some time working from home and they feel intense guilt about it. I'm telling you it is natural! [Medieval peasants worked about half as much as we do now!](https://groups.csail.mit.edu/mac/users/rauch/worktime/hours_workweek.html). [Hunter-gatherers work about half as much as typical office workers!](http://rewild.com/in-depth/leisure.html) I think we're honestly not built for eight hours of constantly coding or fiddling with reports in a day. We've been fooling ourselves.
I think we all need negative space in our days. We need time to rest and think. It's gauche to admit that in our puritan work culture. We need to be constantly in motion and be seen being constantly in motion. So the average office is a dryer full of ping-pong balls all bouncing off of each other, going round and round, but not making much forward progress. In fact, we're all probably melting a little.

View file

@ -4,7 +4,7 @@ title: The Corona Recession is Different
The Great Recession began as a plague of capital. The Collateralized Debt Obligations and Mortgage Backed Securities got sick first. The real economy got sick later once people couldn't afford to stay in their homes but couldn't afford to sell them either.
Throughout the Great Recession, the working class in general, and service workers in particular, kept everything running&mdash; as they always do. Economists call the Service sector \"acyclic\" which means it is comparatively resistant to the boom and bust cycles of capitalism. People didn't stop buying haircuts or eating at restaurants during the Great Recession. You still need haircuts, food, and medical care, even if there is havoc in the fictive economy of the stock markets. The Service sector is our life preserver.
Throughout the Great Recession, the working class in general, and service workers in particular, kept everything running&mdash; as they always do. Economists call the Service sector "acyclic" which means it is comparatively resistant to the boom and bust cycles of capitalism. People didn't stop buying haircuts or eating at restaurants during the Great Recession. You still need haircuts, food, and medical care, even if there is havoc in the fictive economy of the stock markets. The Service sector is our life preserver.
The corona recession is a consequence of an actual material plague. People are getting sick first, then the economy is getting sick because people can't show up for work. The Service sector kept us afloat last time, but we need to shut it down for material epidemiological reasons. We are about to be adrift without our life preserver.

View file

@ -6,9 +6,9 @@ I just finished reading Buckminster Fuller's *Operating Manual for Spaceship Ear
My capsule review: The book puts forward a fascinating theory of history and our position in the universe in a concise and engaging package.
As I read the book, my mind kept finding lines between Fuller and eco-socialism or eco-anarchism. While Fuller calls capitalism and socialism \"mutually extinct,\" there are some ideas in his work that wouldn't be out of place in Marx, Proudhon, or certainly Bookchin.
As I read the book, my mind kept finding lines between Fuller and eco-socialism or eco-anarchism. While Fuller calls capitalism and socialism "mutually extinct," there are some ideas in his work that wouldn't be out of place in Marx, Proudhon, or certainly Bookchin.
For example: Fuller's Great Pirates theory of history is a first cousin of \"the history of man is the history of class struggle.\" The Great Pirate theory describes the separation of the ruling class from the ruled class based on who labors and who can command labor. Fuller follows this thread through all the mutations of history&mdash; chieftains to kings to industrialists&mdash; until we reach the current epoch. In recent history, the Great Pirates&mdash; the members of the ruling class&mdash; have delegated management of their affairs to states and the scientific-professional class. This is an analysis of our situation that would not be out of place in Piketty.
For example: Fuller's Great Pirates theory of history is a first cousin of "the history of man is the history of class struggle." The Great Pirate theory describes the separation of the ruling class from the ruled class based on who labors and who can command labor. Fuller follows this thread through all the mutations of history&mdash; chieftains to kings to industrialists&mdash; until we reach the current epoch. In recent history, the Great Pirates&mdash; the members of the ruling class&mdash; have delegated management of their affairs to states and the scientific-professional class. This is an analysis of our situation that would not be out of place in Piketty.
Fuller correctly concludes that war is a way for the upper class to drive demand and production while controlling the lower classes with xenophobia and violence. He argues that nations and borders are inefficient relics that draw arbitrary unreal lines between people for the purpose of encouraging war and its economic activity. As an engineer, he sees that nations and borders create a tremendous amount of redundancy&mdash; redundancy which is useful if you are a Great Pirate who needs production to continue ceaseless to ensure your continued wealth and power.
@ -22,6 +22,6 @@ As with all futurism, Fuller's predictive track record is mixed. He predicted th
Fuller couldn't understand why a person with functionally limitless wealth wouldn't start sharing at some point. There comes a point in wealth accumulation when all your barriers are lifted: you have all the creature comforts that you could want, you can travel as much as you would like, and you can work as little as you please. It isn't rational to hoard wealth beyond this point. Fuller, a thoroughly rational person, concludes that the thing to do at that point is to stop seeking further wealth, enjoy what you have, and let the rest flow to everyone else who isn't there yet. He failed to consider that for some people, enough is never enough. Those people seek out wealth, and sometimes they get it. Once they have it, they use their wealth to get more wealth. Since their hunger is infinite, they keep using their wealth to capture more wealth until they have as much as possible, and everyone else is driven to the line of subsistence. The system is inexorable. Even if any individual were to choose differently and give away all their hoarded wealth, another yawning pit of greed would open to fill the gap.
I'm surprised that Fuller missed this, considering he had the Great Pirate theory and whole-systems-thinking right there in the same volume. The Great Pirates keep their fleets because the fleets are power and Great Pirates want power. He spent large stretches of *Operating Manual* discussing how the whole physical world&mdash; galaxy upon galaxy&mash; doesn't constitute a complete system without the metaphysical world of human thought and behavior. The universe acts on us and we act on the universe and then this flow continues round and round forever. If the system of \"human ingenuity plus near-limitless solar energy\" is generally pointed toward a future of universal wealth and leisure, shouldn't we consider that the human element would nudge the ultimate result one way or the other? We should.
I'm surprised that Fuller missed this, considering he had the Great Pirate theory and whole-systems-thinking right there in the same volume. The Great Pirates keep their fleets because the fleets are power and Great Pirates want power. He spent large stretches of *Operating Manual* discussing how the whole physical world&mdash; galaxy upon galaxy&mash; doesn't constitute a complete system without the metaphysical world of human thought and behavior. The universe acts on us and we act on the universe and then this flow continues round and round forever. If the system of "human ingenuity plus near-limitless solar energy" is generally pointed toward a future of universal wealth and leisure, shouldn't we consider that the human element would nudge the ultimate result one way or the other? We should.
*Operating Manual for Spaceship Earth* gives us a lot to consider in a scant 150 pages. I highly recommend it for anyone interested in considering our position in time and space, in realms physical and metaphysical.

View file

@ -2,6 +2,6 @@
title: The Only Way to Wrap an Extension Cord
---
<iframe frameborder=\"0\" scrolling=\"no\" marginheight=\"0\" marginwidth=\"0\"width=\"788.54\" height=\"443\" type=\"text/html\" src=\"https://www.youtube.com/embed/kda4DPAn3C4?autoplay=0&fs=0&iv_load_policy=3&showinfo=0&rel=0&cc_load_policy=0&start=0&end=0&origin=https://youtubeembedcode.com\"><div><small><a href=\"https://youtubeembedcode.com/de/\">youtubeembedcode de</a></small></div><div><small><a href=\"http://add-link-exchange.com\">addlink-exchange</a></small></div></iframe>
<iframe frameborder="0" scrolling="no" marginheight="0" marginwidth="0"width="788.54" height="443" type="text/html" src="https://www.youtube.com/embed/kda4DPAn3C4?autoplay=0&fs=0&iv_load_policy=3&showinfo=0&rel=0&cc_load_policy=0&start=0&end=0&origin=https://youtubeembedcode.com"><div><small><a href="https://youtubeembedcode.com/de/">youtubeembedcode de</a></small></div><div><small><a href="http://add-link-exchange.com">addlink-exchange</a></small></div></iframe>
You are welcome.

View file

@ -2,11 +2,11 @@
title: "On \"Time Famine\""
---
I was recently introduced to the concept of [\"time famine\"](https://www.cnn.com/2017/07/24/health/time-famine-stress-happiness-study/index.html), which is:
I was recently introduced to the concept of ["time famine"](https://www.cnn.com/2017/07/24/health/time-famine-stress-happiness-study/index.html), which is:
> the universal feeling of having too much to do but not enough time to deal with those demands.
I know the feeling. Though the term has been around since 1999 or so, there's been a spike in discussion lately. The focus of that discussion has been on finding ways to save time, mostly via automation products and delivery services. It's almost as if there are people with a vested interest in selling you a \"solution\" to this problem (whether it works or not). I think most of the discussion and most of the proposed solutions entirely miss the point.
I know the feeling. Though the term has been around since 1999 or so, there's been a spike in discussion lately. The focus of that discussion has been on finding ways to save time, mostly via automation products and delivery services. It's almost as if there are people with a vested interest in selling you a "solution" to this problem (whether it works or not). I think most of the discussion and most of the proposed solutions entirely miss the point.
It's natural to look at your todo list and despair. It's _particularly_ natural to despair when you look at your todo list over the span of days, months, and years and never see the numbers tick down. It's _natural_ but it's also _wrong_.
@ -22,4 +22,4 @@ How can you stop feeling bad about your task pipeline?
- **Say no.** I became much happier after I realized that there were certain hobbies that I can envy from afar, but cannot personally undertake. There are certain social occasions that I wish I could attend, but I simply cannot while keeping my sanity.
- **Embrace an organizational system that is about _flow_, not _zero_.** I'm very partial to David Allen's Getting Things Done methodology, which probably saved me from death by anxiety and depression. That's a big statement and I mean it.
I'm certainly not immune to feeling \"time famine.\" The difference with this mindset and these tools is that I feel it less often. When I do, I notice the thoughts and can quash the anxiety.
I'm certainly not immune to feeling "time famine." The difference with this mindset and these tools is that I feel it less often. When I do, I notice the thoughts and can quash the anxiety.

View file

@ -89,16 +89,16 @@ If we want to write a test for a function that uses our Weather API, we need to
defmodule UserTest do
# standard test boilerplate as before
test \"current_weather/1 gives the current weather for the user\" do
user = %User{zip_code: \"19120\"}
test "current_weather/1 gives the current weather for the user" do
user = %User{zip_code: "19120"}
WeatherMock
|> expect(:current_weather, fn \"19120\" ->
|> expect(:current_weather, fn "19120" ->
# I've heard it is always sunny there
%{\"description\" => \"clear\"}
%{"description" => "clear"}
end)
assert %{\"description\" => \"clear\"} = User.current_weather(user)
assert %{"description" => "clear"} = User.current_weather(user)
end
end
```

View file

@ -2,6 +2,6 @@
title: We Have Armin van Buuren
---
![We Have Armin van Buuren](https: //www.dropbox.com/s/adwilchh74aa48d/Screen%20Shot%202021-11-29%20at%209.50.25%20AM.png?dl=1)
![We Have Armin van Buuren](https://www.dropbox.com/s/adwilchh74aa48d/Screen%20Shot%202021-11-29%20at%209.50.25%20AM.png?dl=1)
So… is this a hostage situation oorrr…?

View file

@ -3,5 +3,5 @@ title: Three Things I Like This Week
---
1. I've been digging [Oxide and Friends](https: //oxide.computer/) from [Oxide Computer Company](https://oxide.computer/). Oxide is doing something very difficult-- building completely custom, completely secure (from the 1st instruction) servers. Oxide and Friends is the team chatting about the interesting parts of that monumental challenge. I only understand a fraction of what they are talking about, but it is still fascinating. It makes me wish I had finished my Computer Engineering degree. My favorite episode so far is [the episode about the various circuit boards they've designed both for the server AND as tools to doing their work in the lab.](https://www.youtube.com/watch?v=XmiWIlFvSYs).
2. [omg.lol](https://home.omg.lol/) is a delightful place to \"get the best internet address that youve ever had.\" What that means is that it is a simple and very cute service that provides linktree-style profile pages, email accounts, fediverse accounts, link shortening, and more with one subscription-- all attached to the handle you choose for yourself. You can use their URLs, or attach the service to your own domain.
3. I've been enjoying Becky Chambers's [A Psalm for the Wild-Built](https://bookshop.org/p/books/a-psalm-for-the-wild-built-becky-chambers/15125608?ean=9781250236210). The story follows a tea monk whose life is upended when they meet the first robot anyone has seen seen the robots walked off the job and into the wilderness centuries ago. The robot brings with it a seemingly simple and practically impossible-to-answer question: \"what do people need?\"
2. [omg.lol](https://home.omg.lol/) is a delightful place to "get the best internet address that youve ever had." What that means is that it is a simple and very cute service that provides linktree-style profile pages, email accounts, fediverse accounts, link shortening, and more with one subscription-- all attached to the handle you choose for yourself. You can use their URLs, or attach the service to your own domain.
3. I've been enjoying Becky Chambers's [A Psalm for the Wild-Built](https://bookshop.org/p/books/a-psalm-for-the-wild-built-becky-chambers/15125608?ean=9781250236210). The story follows a tea monk whose life is upended when they meet the first robot anyone has seen seen the robots walked off the job and into the wilderness centuries ago. The robot brings with it a seemingly simple and practically impossible-to-answer question: "what do people need?"

View file

@ -2,7 +2,7 @@
title: Three Things I Like This Week (2023-02-03)
---
- The Weakerthans's 2003 album _[Reconstruction Site](https: //music.youtube.com/playlist?list=OLAK5uy_n4E9NKbNFgsgJQLZczatKfAodn6SXbWr4)_. One of my intentions for this year is to listen to more complete albums instead of hopping from single to single. I've loved this album since it came out, but I rediscovered it this week after the track Plea from a Cat Named Virtute spontaneously appeared in my brain again. Every single track is good, but the album is even better as a whole.
- The Weakerthans's 2003 album _[Reconstruction Site](https://music.youtube.com/playlist?list=OLAK5uy_n4E9NKbNFgsgJQLZczatKfAodn6SXbWr4)_. One of my intentions for this year is to listen to more complete albums instead of hopping from single to single. I've loved this album since it came out, but I rediscovered it this week after the track _Plea from a Cat Named Virtute_ spontaneously appeared in my brain again. Every single track is good, but the album is even better as a whole.
- Consumer Reports released an app called [Permission Slip](https://www.permissionslipcr.com/) that lets you easily opt-out of data collection and sharing with many companies with a few simple taps. In some cases, you can even request that a company delete your data entirely. They estimate that they've saved their users something like 150,000 hours since launch. Amazing and totally free.
- KeokeN Interactive's [Deliver Us the Moon](https://store.epicgames.com/en-US/p/deliver-us-the-moon) is a sci-fi thriller adventure puzzle game. The Earth has been totally depleted of resources, and humanity's only hope for long-term survival seemed to be a Moon-based fusion reactor and the Microwave Power Transmission (MPT) system. Then five years ago, without any warning or explanation, the lunar colonies stopped transmitting and the MPT failed, blacking out the whole world. You play as a lone astronaut sent on a longshot mission to investigate what happened and bring the MPT back on line. I became aware of this game when a sequel (Deliver Us Mars) was released this week.

View file

@ -4,13 +4,13 @@ title: "How I Get Fractional Lead Contracts Without \"Doing Sales\""
I am often asked how I find the clients for my Fractional Engineering Lead practice. Here's what I do. It has worked for me for about two and a half years now, but it might not work for you and it might not even work for me a month from now.
I don't do sales. I don't do \"lead gen\" or \"biz dev.\" I don't even really network in the traditional sense. I also don't have someone to do these things for me.
I don't do sales. I don't do "lead gen" or "biz dev." I don't even really network in the traditional sense. I also don't have someone to do these things for me.
What I do is I help people. I don't help them so that they give me contracts some day. I help them because I like helping people, particularly other Software Engineers, and I help them because it's the right thing to do.
The other thing I do is I ask people to help me. I don't ask for contracts, because either someone has work that's a good fit for me, or they don't. I ask people to give me advice, to help troubleshoot an issue, or to introduce me to their friends.
Every contract I've ever gotten has been someone who I've helped and/or someone who has helped me. It's usually not anytime close to when we had that first interaction. They usually email me up out of the blue and say \"I have / heard about a project, and I think you could help.\"
Every contract I've ever gotten has been someone who I've helped and/or someone who has helped me. It's usually not anytime close to when we had that first interaction. They usually email me up out of the blue and say "I have / heard about a project, and I think you could help."
When I started working as a Fractional Engineering Lead, I got my first contract from an amazing kind former coworker of mine. I helped her (in a very small way) to get that job, and she helped me fix some HR snags at that company. A couple years later, she heard that I'd lost my job and she connected me to my first incredible client. I don't think I would have the business I have today without her!
@ -20,4 +20,4 @@ Ok, but you probably want some practical steps you can take. That's very reasona
2. Fix bugs in open source software. Particularly fix the bugs that no one else wants to fix.
3. Re-post jobs and the posts of those seeking work. Proactively connect people you know to jobs that are open, and vice-versa.
4. Ask for calls with people you like and respect. Ask for advice, and ask if there's anything you can offer in exchange. If what you need is a contract, be honest. _Hey, I'm trying my hand at consulting, I'd like to have a short call to (1) get your advice (2) tell you what I'm trying to do and see if you know anyone who could benefit._
5. Don't filter people out just because they're \"not the target demographic\" or whatever. That's sales. If you say to yourself \"I shouldn't talk to this person, because they probably don't need my services\" then you are doing sales. _Don't sell._ Everyone on this earth needs help, and in my experience, the vast majority of people on this planet want to help others.
5. Don't filter people out just because they're "not the target demographic" or whatever. That's sales. If you say to yourself "I shouldn't talk to this person, because they probably don't need my services" then you are doing sales. _Don't sell._ Everyone on this earth needs help, and in my experience, the vast majority of people on this planet want to help others.

View file

@ -3,6 +3,7 @@
"/home/prehnra/github",
"/home/prehnra/src",
"/home/prehnra/source",
"/home/prehnra/projects",
"/home/prehnra/treesitter"
],
"theme": {