Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .dwollaci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
stages:
enableCD: true
build:
nodeLabel: sbt
steps:
- |
export SDKMAN_DIR="$HOME/.sdkman"
mkdir -p "${SDKMAN_DIR}/candidates/java/current/bin"
set +o xtrace
. "${SDKMAN_DIR}/bin/sdkman-init.sh"
sdk env install use
set -o xtrace
sbt test
sbt doc
sbt npmPackage
filesToStash:
- '**'
deployProd:
nodeLabel: nvm-sbt-deployer
steps:
- |
./deploy.sh

63 changes: 63 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# This file was automatically generated by sbt-github-actions using the
# githubWorkflowGenerate task. You should add and commit this file to
# your git repository. It goes without saying that you shouldn't edit
# this file by hand! Instead, if you wish to make changes, you should
# change your sbt build configuration to revise the workflow description
# to meet your needs, then regenerate this file.

name: Continuous Integration

on:
pull_request:
branches: ['**']
push:
branches: ['**']

env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


concurrency:
group: ${{ github.workflow }} @ ${{ github.ref }}
cancel-in-progress: true

jobs:
build:
name: Test
strategy:
matrix:
os: [ubuntu-22.04]
scala: [3]
java: [temurin@11]
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- name: Checkout current branch (full)
uses: actions/checkout@v5
with:
fetch-depth: 0

- name: Setup sbt
uses: sbt/setup-sbt@v1

- name: Setup Java (temurin@11)
id: setup-java-temurin-11
if: matrix.java == 'temurin@11'
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 11
cache: sbt

- name: sbt update
if: matrix.java == 'temurin@11' && steps.setup-java-temurin-11.outputs.cache-hit == 'false'
run: sbt +update

- name: Check that workflows are up to date
run: sbt githubWorkflowCheck

- name: Build project
run: sbt '++ ${{ matrix.scala }}' test

- name: Package
run: sbt '++ ${{ matrix.scala }}' npmPackage
59 changes: 59 additions & 0 deletions .github/workflows/clean.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# This file was automatically generated by sbt-github-actions using the
# githubWorkflowGenerate task. You should add and commit this file to
# your git repository. It goes without saying that you shouldn't edit
# this file by hand! Instead, if you wish to make changes, you should
# change your sbt build configuration to revise the workflow description
# to meet your needs, then regenerate this file.

name: Clean

on: push

jobs:
delete-artifacts:
name: Delete Artifacts
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Delete artifacts
run: |
# Customize those three lines with your repository and credentials:
REPO=${GITHUB_API_URL}/repos/${{ github.repository }}

# A shortcut to call GitHub API.
ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; }

# A temporary file which receives HTTP response headers.
TMPFILE=/tmp/tmp.$$

# An associative array, key: artifact name, value: number of artifacts of that name.
declare -A ARTCOUNT

# Process all artifacts on this repository, loop on returned "pages".
URL=$REPO/actions/artifacts
while [[ -n "$URL" ]]; do

# Get current page, get response headers in a temporary file.
JSON=$(ghapi --dump-header $TMPFILE "$URL")

# Get URL of next page. Will be empty if we are at the last page.
URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*<//' -e 's/>.*//')
rm -f $TMPFILE

# Number of artifacts on this page:
COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') ))

# Loop on all artifacts on this page.
for ((i=0; $i < $COUNT; i++)); do

# Get name of artifact and count instances of this name.
name=$(jq <<<$JSON -r ".artifacts[$i].name?")
ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1))

id=$(jq <<<$JSON -r ".artifacts[$i].id?")
size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") ))
printf "Deleting '%s' #%d, %'d bytes\n" $name ${ARTCOUNT[$name]} $size
ghapi -X DELETE $REPO/actions/artifacts/$id
done
done
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
.idea/
target/
project/project/
node_modules/
.bsp/
package.json
package-lock.json
cdk.out/
17 changes: 17 additions & 0 deletions .mergify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# This file was automatically generated by sbt-typelevel-mergify using the
# mergifyGenerate task. You should add and commit this file to
# your git repository. It goes without saying that you shouldn't edit
# this file by hand! Instead, if you wish to make changes, you should
# change your sbt build configuration to revise the mergify configuration
# to meet your needs, then regenerate this file.

pull_request_rules:
- name: merge scala-steward's PRs
conditions:
- author=dwolla-oss-scala-steward[bot]
- or:
- body~=labels:.*early-semver-patch
- body~=labels:.*early-semver-minor
- status-success=Test (ubuntu-22.04, 3, temurin@11)
actions:
merge: {}
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lts/jod
1 change: 1 addition & 0 deletions .sdkmanrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
java=11.0.28-tem
14 changes: 0 additions & 14 deletions .travis.yml

This file was deleted.

39 changes: 36 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,46 @@ sbt clean 'testOnly -- timefactor 10' 'stack/testOnly -- timefactor 10' stack/it

## Deploy

To deploy the stack, ensure the required IAM roles exist (`DataEncrypter` and `cloudformation/deployer/cloudformation-deployer`), then deploy with `sbt`:
This project uses the [AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/home.html) to
synthesize and deploy the Lambda and its CloudFormation stack. The commands below are
provided by the `CdkDeployPlugin` sbt plugin, which drives the CDK CLI via
`npx --yes aws-cdk@2` — so a local Node.js/npm is required, but the CDK CLI itself does
not need to be installed globally.

### Commands

| Command | Description | AWS credentials |
|---------|-------------|-----------------|
| `sbt cdkSynth` | Builds the Lambda artifact and synthesizes the CloudFormation template to `target/cdk.out`. Makes no AWS calls. | Not required |
| `sbt 'cdkDiff <stage>'` | Synthesizes the stack and shows the diff against what is currently deployed. | Required |
| `sbt 'show deploy <stage>'` | Synthesizes and deploys the stack to AWS. | Required |

`<stage>` is one of `Admin` or `Sandbox` (case-insensitive).

### Target account & region

`cdkDiff` and `deploy` resolve the target environment from two environment variables and
confirm it against your ambient AWS credentials. Both must be set to real values before
deploying or diffing:

```ShellSession
sbt -DAWS_ACCOUNT_ID={your-account-id} publish stack/deploy
export CDK_DEPLOY_ACCOUNT=
export CDK_DEPLOY_REGION=us-west-2
sbt 'show deploy Admin'
```

The `publish` task comes from [Dwolla’s S3 sbt plugin](https://github.com/Dwolla/sbt-s3-publisher), and the stack/deploy task comes from [Dwolla’s CloudFormation sbt plugin](https://github.com/Dwolla/sbt-cloudformation-stack).
`cdkSynth` makes no AWS calls, so it falls back to placeholder values when these are
unset. The target account must have been bootstrapped for the CDK (`cdk bootstrap`) once
before the first deploy.

### Deploy gating & output

The `deploy` task only deploys when the current build's version matches the latest git
tag on `HEAD`; otherwise it is a no-op and reports `SkippedBecauseVersionIsNotLatestTag`.
Wrapping the task in sbt's [`show`](https://www.scala-sbt.org/1.x/docs/Inspecting-Settings.html#show)
prints the resulting outcome (`Success` or `Skipped…`) to the build log.

In CI, deploys run automatically from the `deployProd` stage in [`.dwollaci.yml`](.dwollaci.yml).

## CloudFormation Custom Resource

Expand Down
123 changes: 56 additions & 67 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,77 +1,66 @@
lazy val commonSettings = Seq(
organization := "Dwolla",
homepage := Option(url("https://stash.dwolla.net/projects/OPS/repos/cloudflare-public-hostname-lambda")),
)

lazy val specs2Version = "4.3.0"
lazy val awsSdkVersion = "1.11.475"
import org.typelevel.sbt.gha.WorkflowStep

lazy val `cloudflare-public-hostname-lambda` = (project in file("."))
.settings(
name := "cloudflare-public-hostname-lambda",
resolvers ++= Seq(
Resolver.bintrayRepo("dwolla", "maven")
),
libraryDependencies ++= {
val fs2AwsVersion = "1.3.0"
evictionErrorLevel := Level.Warn

Seq(
"com.dwolla" %% "scala-cloudformation-custom-resource" % "2.1.0",
"com.dwolla" %% "fs2-aws" % fs2AwsVersion,
"io.circe" %% "circe-fs2" % "0.9.0",
"com.dwolla" %% "cloudflare-api-client" % "4.0.0-M4",
"org.http4s" %% "http4s-blaze-client" % "0.18.21",
"com.amazonaws" % "aws-java-sdk-kms" % awsSdkVersion,
"org.apache.httpcomponents" % "httpclient" % "4.5.2",
"org.specs2" %% "specs2-core" % specs2Version % Test,
"org.specs2" %% "specs2-mock" % specs2Version % Test,
"org.specs2" %% "specs2-matcher-extra" % specs2Version % Test,
"com.dwolla" %% "testutils-specs2" % "1.11.0" % Test exclude("ch.qos.logback", "logback-classic"),
"com.dwolla" %% "fs2-aws-testkit" % fs2AwsVersion % Test,
)
},
updateOptions := updateOptions.value.withCachedResolution(false),
)
.settings(commonSettings: _*)
.configs(IntegrationTest)
.settings(Defaults.itSettings: _*)
.enablePlugins(PublishToS3)
ThisBuild / organization := "Dwolla"
ThisBuild / homepage := Option(url("https://github.com/Dwolla/cloudflare-public-hostname-lambda"))
ThisBuild / licenses += ("MIT", url("http://opensource.org/licenses/MIT"))
ThisBuild / scalaVersion := "3.7.4"
ThisBuild / developers := List(
Developer(
"bpholt",
"Brian Holt",
"bholt+github@dwolla.com",
url("https://dwolla.com")
),
)
ThisBuild / resolvers += Resolver.sonatypeCentralSnapshots
ThisBuild / mergifyStewardConfig ~= { _.map {
_.withAuthor("dwolla-oss-scala-steward[bot]")
.withMergeMinors(true)
}}
ThisBuild / githubWorkflowPublishTargetBranches := Seq.empty
ThisBuild / githubWorkflowBuild += WorkflowStep.Sbt(List("npmPackage"), name = Some("Package"))

lazy val stack: Project = (project in file("stack"))
.settings(commonSettings: _*)
lazy val `cloudflare-public-hostname-lambda` = project
.in(file("."))
.settings(
resolvers ++= Seq(Resolver.jcenterRepo),
name := "cloudflare-public-hostname-lambda",
smithy4sAwsSpecs ++= Seq(AWS.kms),
scalacOptions += "-Wconf:src=src_managed/.*:s",
dependencyOverrides += "org.scala-lang" %% "scala3-library" % scalaVersion.value,
libraryDependencies ++= {
val scalaAwsUtilsVersion = "1.6.1"

Seq(
"com.monsanto.arch" %% "cloud-formation-template-generator" % "3.5.4",
"org.specs2" %% "specs2-core" % specs2Version % "test,it",
"com.amazonaws" % "aws-java-sdk-cloudformation" % awsSdkVersion % IntegrationTest,
"com.dwolla" %% "scala-aws-utils" % scalaAwsUtilsVersion % IntegrationTest,
"org.typelevel" %%% "feral-lambda-cloudformation-custom-resource" % "0.3.1",
"org.typelevel" %%% "cats-tagless-core" % "0.16.3",
"com.dwolla" %%% "cloudflare-api-client" % "4.0-e2f7bfc-SNAPSHOT",
"com.dwolla" %%% "natchez-tagless" % "0.2.6",
"com.disneystreaming.smithy4s" %%% "smithy4s-cats" % smithy4sVersion.value,
"com.disneystreaming.smithy4s" %%% "smithy4s-http4s" % smithy4sVersion.value,
"com.disneystreaming.smithy4s" %%% "smithy4s-aws-http4s" % smithy4sVersion.value,
"com.disneystreaming.smithy4s" %%% "smithy4s-json" % smithy4sVersion.value,
"org.http4s" %%% "http4s-ember-client" % "0.23.30",
"org.typelevel" %%% "mouse" % "1.3.2",
"org.tpolecat" %%% "natchez-mtl" % "0.3.8",
"org.tpolecat" %%% "natchez-xray" % "0.3.8",
"org.tpolecat" %%% "natchez-http4s" % "0.6.1",
"org.tpolecat" %%% "natchez-http4s-mtl" % "0.6.1",
"org.typelevel" %%% "log4cats-core" % "2.7.1",
"org.typelevel" %%% "log4cats-js-console" % "2.7.1",
"com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-circe" % "2.38.0",
"org.typelevel" %%% "munit-cats-effect" % "2.1.0" % Test,
"org.scalameta" %%% "munit" % "1.2.0" % Test,
"org.scalameta" %%% "munit-scalacheck" % "1.2.0" % Test,
"org.typelevel" %%% "scalacheck-effect-munit" % "2.1.0-RC1" % Test,
"org.tpolecat" %%% "natchez-testkit" % "0.3.8" % Test,
"org.typelevel" %%% "log4cats-testing" % "2.7.1" % Test,
)
},
stackName := (name in `cloudflare-public-hostname-lambda`).value,
stackParameters := List(
"S3Bucket" → (s3Bucket in `cloudflare-public-hostname-lambda`).value,
"S3Key" → (s3Key in `cloudflare-public-hostname-lambda`).value
),
awsAccountId := sys.props.get("AWS_ACCOUNT_ID"),
awsRoleName := Option("cloudformation/deployer/cloudformation-deployer"),
scalacOptions --= Seq(
"-Xlint:missing-interpolator",
"-Xlint:option-implicit",
buildInfoKeys := Seq[BuildInfoKey](
name,
version,
),
)
.configs(IntegrationTest)
.settings(Defaults.itSettings: _*)
.enablePlugins(CloudFormationStack)
.dependsOn(`cloudflare-public-hostname-lambda`)
buildInfoPackage := "com.dwolla.lambda.cloudflare.record",

assemblyMergeStrategy in assembly := {
case PathList(ps @ _*) if ps.last == "Log4j2Plugins.dat" => sbtassembly.Log4j2MergeStrategy.plugincache
case PathList("META-INF", "MANIFEST.MF") => MergeStrategy.discard
case PathList("log4j2.xml") => MergeStrategy.singleOrError
case _ ⇒ MergeStrategy.first
}
test in assembly := {}
)
.enablePlugins(BuildInfoPlugin, CdkDeployPlugin, LambdaJSPlugin, Smithy4sCodegenPlugin)
Loading