diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for template-hs
+
+## 0.1.0.0 -- YYYY-mm-dd
+
+* First version. Released on an unsuspecting world.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,24 @@
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to <https://unlicense.org>
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,51 @@
+# template.hs
+
+[Homepage][repository]
+
+By Alex Brandt <alunduil@gmail.com>
+
+## Description
+
+You can use template.hs to create a new GitHub repository. The repository will
+have Haskell, VS Code devcontainers, and various GitHub actions ready to use.
+
+## Terms of use
+
+You are free to use template.hs as a basis for your own projects without any
+conditions.  See the [LICENSE] file for details.
+
+## Prerequisites
+
+1. VS Code with "Remote Development" installed
+
+## How to use this template
+
+1. Visit [the repository][repository]
+1. Click "Use this template"
+1. Follow the GitHub Docs to [Create a repo][create a repo]
+1. Open VS Code
+1. Open the command prompt (ctrl+shift+p)
+1. Type "clone repository in container" and hit return
+1. Input the GitHub URL of your new repository
+1. In the resulting terminal (ctrl+\`), run: `cabal run initialise`
+1. Resolve the README update issue that is generated
+1. Continue working on your awesome project
+
+## Documentation
+
+* [LICENSE]: The license governing use of template.hs
+
+## Getting Help
+
+* [GitHub Issues][issues]: Support requests, bug reports, and feature requests
+
+## How to Help
+
+* Submit [issues] for problems or questions
+* Submit [pull requests] for proposed changes
+
+[create a repo]: https://docs.github.com/en/get-started/quickstart/create-a-repo
+[issues]: https://github.com/alunduil/template.hs/issues
+[LICENSE]: ./LICENSE
+[pull requests]: https://github.com/alunduil/template.hs/pulls
+[repository]: https://github.com/alunduil/template.hs
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,3 @@
+import Distribution.Simple
+
+main = defaultMain
diff --git a/bin/initialise/Main.hs b/bin/initialise/Main.hs
new file mode 100644
--- /dev/null
+++ b/bin/initialise/Main.hs
@@ -0,0 +1,41 @@
+module Main (main, main') where
+
+import qualified Cabal (replace)
+import qualified Configuration (parser)
+import qualified Defaults (getDefaults)
+import qualified File (replace)
+import Initialise (Initialise, runInitialise)
+import qualified Licence (replace)
+import Options.Applicative (execParser, fullDesc, helper, info, progDesc, (<**>))
+import System.FilePath ((</>))
+
+main :: IO ()
+main = do
+  defaults <- Defaults.getDefaults
+
+  let options =
+        info
+          (Configuration.parser defaults <**> helper)
+          ( fullDesc
+              <> progDesc
+                ( unlines
+                    [ "Initialise a new project using the current checked out repository.",
+                      "WARNING: THIS WILL MODIFY THE CURRENT CONTENTS OF YOUR CHECKED OUT REPOSITORY!"
+                    ]
+                )
+          )
+
+  execParser options >>= runInitialise main'
+
+main' :: Initialise ()
+main' = do
+  mapM_
+    File.replace
+    [ ".devcontainer" </> "devcontainer.json",
+      ".github" </> "workflows" </> "haskell-ci.yml",
+      "CHANGELOG.md"
+    ]
+  Licence.replace "LICENSE"
+  Cabal.replace "templatise.cabal"
+
+-- TODO README
diff --git a/lib/initialise/Cabal.hs b/lib/initialise/Cabal.hs
new file mode 100644
--- /dev/null
+++ b/lib/initialise/Cabal.hs
@@ -0,0 +1,147 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# OPTIONS_GHC -Wno-orphans #-}
+
+module Cabal (replace, convert) where
+
+import Configuration (Configuration (..))
+import Control.Exception (Exception)
+import Control.Monad.Catch (throwM)
+import Control.Monad.Reader (asks, liftIO)
+import Data.ByteString (ByteString, append, breakSubstring, concat, readFile, stripPrefix)
+import Data.ByteString.Char8 (pack)
+import Data.Text (Text, unpack)
+import Data.Text.Encoding (encodeUtf8)
+import Distribution.Fields
+  ( CommentPosition (NoComment),
+    Field (Field, Section),
+    FieldLine (FieldLine),
+    Name (Name),
+    SectionArg (SecArgName, SecArgStr),
+    fromParsecFields,
+    readFields,
+    showFields,
+  )
+import Distribution.Fields.Field (fieldLineAnn)
+import Distribution.Parsec.Position (Position)
+import Distribution.SPDX (licenseId)
+import Initialise (Initialise)
+import System.Directory.Extra (createDirectoryIfMissing, removeDirectoryRecursive, removeFile)
+import System.FilePath (replaceBaseName, (</>))
+import Text.Parsec.Error (ParseError)
+import Prelude hiding (concat, readFile)
+
+instance Exception ParseError
+
+replace :: FilePath -> Initialise ()
+replace path = do
+  replaceCabal path
+  "lib" `replaceDirectoryWith` replaceLib
+  "test" `replaceDirectoryWith` replaceTest
+  "bin" `replaceDirectoryWith` replaceBin
+
+replaceCabal :: FilePath -> Initialise ()
+replaceCabal path = do
+  -- TODO handle in replaceWith
+  path' <- asks (flip replaceBaseName path . unpack . name)
+  -- TODO replaceWith convert
+  contents <- liftIO $ readFile path
+  contents' <- convert contents
+  liftIO $ writeFile path' contents'
+  -- TODO handle in replaceWith
+  liftIO $ removeFile path
+
+convert :: ByteString -> Initialise String
+convert contents = do
+  fs <- either throwM pure (readFields contents)
+  showFields (const NoComment) . fromParsecFields <$> mapM convert' fs
+
+convert' :: Field Position -> Initialise (Field Position)
+convert' f@(Field n@(Name _ fName) ls) = do
+  Configuration {..} <- asks id
+  case fName of
+    -- package
+    "name" -> field (encodeUtf8 name)
+    "version" -> field "0.1.0.0"
+    "license" -> field (pack $ licenseId licence)
+    "copyright" -> field (pack $ unwords ["(c)", show year, unpack author])
+    "author" -> field (encodeUtf8 author)
+    "maintainer" -> field (encodeUtf8 maintainer)
+    "homepage" -> field $ pack $ show homepage
+    "bug-reports" -> field $ pack (show homepage ++ "/issues")
+    "synopsis" -> field "TODO"
+    "description" -> field "TODO"
+    -- common
+    "import" -> field (encodeUtf8 name `append` "-common")
+    "exposed-modules" -> field ""
+    "other-modules" -> field ""
+    "build-depends" -> pure $ Field n $ map (convertFieldLine name) ls
+    "hs-source-dirs" -> pure $ Field n $ map (convertFieldLine name) ls
+    -- source-repository
+    "location" -> field $ pack $ show homepage
+    _ -> pure f
+  where
+    field s = pure $ Field n [FieldLine annotation s]
+    annotation = fieldLineAnn . head $ ls
+convert' (Section n arguments fs) = do
+  Configuration {..} <- asks id
+  fs' <- mapM convert' fs
+  pure $ Section n (map (convertSectionArgument name) arguments) fs'
+
+convertSectionArgument :: Text -> SectionArg Position -> SectionArg Position
+convertSectionArgument n s = case s of
+  (SecArgName a o) -> SecArgName a $ convertString n o
+  (SecArgStr a o) -> SecArgStr a $ convertString n o
+  _ -> s
+
+convertFieldLine :: Text -> FieldLine Position -> FieldLine Position
+convertFieldLine r (FieldLine annotation s) = FieldLine annotation $ convertString r s
+
+convertString :: Text -> ByteString -> ByteString
+convertString r s = case token `stripPrefix` rest of
+  Just suffix -> concat [prefix, encodeUtf8 r, suffix]
+  Nothing -> s
+  where
+    (prefix, rest) = token `breakSubstring` s
+    token = "initialise"
+
+-- TODO Move to Initialise module?
+replaceDirectoryWith :: FilePath -> (FilePath -> Initialise ()) -> Initialise ()
+replaceDirectoryWith component r = do
+  name' <- asks name
+  path' <- asks ((</> component </> unpack name') . path)
+  liftIO $ createDirectoryIfMissing True path'
+  r path'
+  liftIO $ removeDirectoryRecursive $ replaceBaseName "initialise" path'
+
+replaceLib :: FilePath -> Initialise ()
+replaceLib _path = pure ()
+
+replaceTest :: FilePath -> Initialise ()
+replaceTest path = do
+  name' <- asks name
+  -- TODO Template library.
+  liftIO $
+    writeFile (path </> "Main.hs") $
+      unlines
+        [ "module Main (main) where",
+          "",
+          "import Test.Tasty (defautMain, testGroup)",
+          "",
+          "main :: IO ()",
+          "main = defaultMain $ testGroup \"" ++ unpack name' ++ "-library\" []"
+        ]
+
+replaceBin :: FilePath -> Initialise ()
+replaceBin path = do
+  name' <- asks name
+  -- TODO Template library.(*)
+  liftIO $
+    writeFile (path </> "Main.hs") $
+      unlines
+        [ "module Main (main) where",
+          "",
+          "main :: IO ()",
+          "main = putStrLn " ++ unpack name'
+        ]
diff --git a/lib/initialise/Configuration.hs b/lib/initialise/Configuration.hs
new file mode 100644
--- /dev/null
+++ b/lib/initialise/Configuration.hs
@@ -0,0 +1,99 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+
+module Configuration
+  ( Configuration (..),
+    parser,
+  )
+where
+
+import Data.Text (Text)
+import Data.Time.Calendar (Year)
+import Defaults (Defaults (..), dHomePage, dName)
+import Distribution.SPDX.LicenseId (LicenseId (Unlicense))
+import Network.URI (URI, parseURI)
+import Options.Applicative
+  ( Parser,
+    auto,
+    help,
+    hidden,
+    internal,
+    long,
+    metavar,
+    option,
+    showDefault,
+    strOption,
+    value,
+  )
+import Options.Applicative.Builder (maybeReader)
+
+data Configuration = Configuration
+  { name :: Text,
+    homepage :: URI,
+    author :: Text,
+    maintainer :: Text,
+    licence :: LicenseId,
+    path :: FilePath,
+    year :: Year
+  }
+
+parser :: Defaults -> Parser Configuration
+parser ds@(Defaults {..}) =
+  Configuration
+    <$> strOption
+      ( long "name"
+          <> help "Name of the new project."
+          <> metavar "NAME"
+          <> value (dName ds)
+          <> showDefault
+      )
+    <*> option
+      (maybeReader parseURI)
+      ( long "homepage"
+          <> help "Homepage of the new project."
+          <> metavar "URL"
+          <> value (dHomePage ds)
+          <> showDefault
+      )
+    <*> strOption
+      ( long "author"
+          <> help "Name of the author of the project."
+          <> metavar "AUTHOR"
+          <> value dAuthor
+          <> showDefault
+      )
+    <*> strOption
+      ( long "maintainer"
+          <> help "Email of the maintainer of the project."
+          <> metavar "MAINTAINER"
+          <> value dMaintainer
+          <> showDefault
+      )
+    <*> option
+      auto
+      ( long "licence"
+          <> help "Licence of the project."
+          <> value Unlicense
+          <> showDefault
+          <> metavar "LICENCE"
+      )
+    <*> option
+      auto
+      ( long "path"
+          <> help "Project path.  Only used for testing."
+          <> value dPath
+          <> showDefault
+          <> metavar "PATH"
+          <> hidden
+          <> internal
+      )
+    <*> option
+      auto
+      ( long "year"
+          <> help "Copyright year.  Only used for testing."
+          <> value dYear
+          <> showDefault
+          <> metavar "YEAR"
+          <> hidden
+          <> internal
+      )
diff --git a/lib/initialise/Defaults.hs b/lib/initialise/Defaults.hs
new file mode 100644
--- /dev/null
+++ b/lib/initialise/Defaults.hs
@@ -0,0 +1,47 @@
+{-# LANGUAGE RecordWildCards #-}
+
+module Defaults
+  ( getDefaults,
+    Defaults (..),
+    dName,
+    dHomePage,
+  )
+where
+
+import Data.Maybe (fromJust)
+import Data.Text (Text, pack, unpack)
+import Data.Time (LocalTime (localDay), getCurrentTime, getCurrentTimeZone, utcToLocalTime)
+import Data.Time.Calendar (Year)
+import Data.Time.Calendar.OrdinalDate (toOrdinalDate)
+import qualified Git (config)
+import Network.URI (URI (uriPath), parseURI)
+import System.Directory.Extra (getCurrentDirectory)
+import System.FilePath (dropExtension, takeBaseName)
+
+data Defaults = Defaults
+  { dOrigin :: URI,
+    dAuthor :: Text,
+    dMaintainer :: Text,
+    dPath :: FilePath,
+    dYear :: Year
+  }
+  deriving (Show, Eq)
+
+dName :: Defaults -> Text
+dName Defaults {..} = pack . takeBaseName . uriPath $ dOrigin
+
+dHomePage :: Defaults -> URI
+dHomePage Defaults {..} =
+  dOrigin
+    { uriPath = dropExtension $ uriPath dOrigin
+    }
+
+getDefaults :: IO Defaults
+getDefaults = do
+  dOrigin <- fromJust . parseURI . unpack <$> Git.config "remote.origin.url"
+  dAuthor <- Git.config "user.name"
+  dMaintainer <- Git.config "user.email"
+  dPath <- getCurrentDirectory
+  timezone <- getCurrentTimeZone
+  (dYear, _day) <- toOrdinalDate . localDay . utcToLocalTime timezone <$> getCurrentTime
+  pure Defaults {..}
diff --git a/lib/initialise/File.hs b/lib/initialise/File.hs
new file mode 100644
--- /dev/null
+++ b/lib/initialise/File.hs
@@ -0,0 +1,28 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+
+module File (replace, convert) where
+
+import Configuration (Configuration (..))
+import Control.Monad.Reader (MonadIO (liftIO), MonadReader (ask))
+import Data.Text (Text)
+import qualified Data.Text as T (replace)
+import Data.Text.IO (readFile, writeFile)
+import Initialise (Initialise)
+import Prelude hiding (readFile, writeFile)
+
+replace :: FilePath -> Initialise ()
+replace path = do
+  -- TODO replaceWith convert
+  contents <- liftIO $ readFile path
+  contents' <- convert contents
+  liftIO $ writeFile path contents'
+
+convert :: Text -> Initialise Text
+convert contents = do
+  Configuration {..} <- ask
+  pure
+    . T.replace "templatise" name
+    . T.replace "template-hs" name
+    . T.replace "template.hs" name
+    $ contents
diff --git a/lib/initialise/Git.hs b/lib/initialise/Git.hs
new file mode 100644
--- /dev/null
+++ b/lib/initialise/Git.hs
@@ -0,0 +1,7 @@
+module Git (config) where
+
+import Data.Text (Text, pack)
+import System.Process (readProcess)
+
+config :: String -> IO Text
+config key = pack . head . lines <$> readProcess "git" ["config", key] ""
diff --git a/lib/initialise/Initialise.hs b/lib/initialise/Initialise.hs
new file mode 100644
--- /dev/null
+++ b/lib/initialise/Initialise.hs
@@ -0,0 +1,13 @@
+module Initialise
+  ( Initialise,
+    runInitialise,
+  )
+where
+
+import Configuration (Configuration)
+import Control.Monad.Reader (ReaderT, runReaderT)
+
+type Initialise = ReaderT Configuration IO
+
+runInitialise :: Initialise a -> Configuration -> IO a
+runInitialise = runReaderT
diff --git a/lib/initialise/Licence.hs b/lib/initialise/Licence.hs
new file mode 100644
--- /dev/null
+++ b/lib/initialise/Licence.hs
@@ -0,0 +1,25 @@
+{-# LANGUAGE RecordWildCards #-}
+
+module Licence (replace) where
+
+import Configuration (Configuration (..))
+import Control.Monad (unless)
+import Control.Monad.Reader (ask, liftIO)
+import Data.ByteString.Lazy (ByteString, writeFile)
+import Distribution.SPDX.LicenseId (LicenseId (Unlicense), licenseId)
+import Initialise (Initialise)
+import Network.HTTP.Client (responseBody)
+import Network.HTTP.Simple (httpLBS, parseRequest)
+import System.FilePath ((</>))
+import Prelude hiding (writeFile)
+
+replace :: FilePath -> Initialise ()
+replace p = do
+  Configuration {..} <- ask
+  unless (licence == Unlicense) $
+    liftIO (writeFile (p </> path) =<< contents licence)
+
+contents :: LicenseId -> IO ByteString
+contents l = do
+  request <- parseRequest $ "https://spdx.org/licenses/" ++ licenseId l ++ ".txt"
+  responseBody <$> httpLBS request
diff --git a/templatise.cabal b/templatise.cabal
new file mode 100644
--- /dev/null
+++ b/templatise.cabal
@@ -0,0 +1,122 @@
+cabal-version:      3.0
+name:               templatise
+version:            0.1.0.1
+license:            Unlicense
+license-file:       LICENSE
+copyright:          (c) 2023 Alex Brandt
+author:             Alex Brandt
+maintainer:         alunduil@gmail.com
+stability:          alpha
+homepage:           https://github.com/alunduil/template.hs
+bug-reports:        https://github.com/alunduil/template.hs/issues
+synopsis:
+  You can use template.hs to create a new Haskell GitHub repository.
+
+description:
+  You can use template.hs to create a new GitHub repository. The repository will
+  have Haskell, VS Code devcontainers, and various GitHub actions ready to use.
+
+category:           VSCode
+tested-with:        GHC ==9.2.8 || ==9.4.5 || ==9.4.6 || ==9.4.7 || ==9.6.2
+extra-source-files:
+  CHANGELOG.md
+  README.md
+
+data-files:
+  test/**/*.cabal
+  test/**/*.json
+  test/**/*.md
+  test/**/*.yml
+
+source-repository head
+  type:     git
+  location: https://github.com/alunduil/template.hs
+
+common initialise-common
+  build-depends:
+    , base                  ^>=4.16.4.0 || ^>=4.17.0.0 || ^>=4.18.0.0
+    , bytestring            ^>=0.11.4.0 || ^>=0.12.0.2
+    , Cabal-syntax          ^>=3.8.1.0  || ^>=3.10.1.0
+    , filepath              ^>=1.4.2.2
+    , mtl                   ^>=2.2.2    || ^>=2.3.1
+    , network-uri           ^>=2.6.4.1  || ^>=2.7.0.0
+    , optparse-applicative  ^>=0.18.1.0
+    , process               ^>=1.6.16.0
+    , text                  ^>=1.2.5.0  || ^>=2.0.2
+
+library initialise-library
+  import:           initialise-common
+  exposed-modules:
+    Cabal
+    Configuration
+    Defaults
+    File
+    Git
+    Initialise
+    Licence
+
+  -- TODO Make Git private.
+  --other-modules: Git
+  build-depends:
+    , exceptions    ^>=0.10.4
+    , extra         ^>=1.7.14
+    , http-client   ^>=0.7.14
+    , http-conduit  ^>=2.3.8.3
+    , parsec        ^>=3.1.15.0
+    , time          ^>=1.11.1.1 || ^>=1.12.2
+
+  hs-source-dirs:   lib/initialise
+
+  --default-extensions:
+  --other-extensions:
+  default-language: Haskell2010
+
+  --build-tool-depends:
+  ghc-options:      -Wall
+
+test-suite initialise-test
+  import:           initialise-common
+  type:             exitcode-stdio-1.0
+  main-is:          Main.hs
+  build-depends:
+    , directory           ^>=1.3.6.2
+    , hspec               ^>=2.11.4
+    , initialise-library
+    , tasty               ^>=1.4.3
+    , tasty-golden        ^>=2.3.5
+    , tasty-hspec         ^>=1.2.0.4
+    , temporary           ^>=1.3
+
+  other-modules:
+    CabalGolden
+    ConfigurationSpec
+    DefaultsSpec
+    FileGolden
+    GitSpec
+    Hooks
+
+  hs-source-dirs:   test/initialise
+
+  --default-extensions:
+  -- other-extensions:
+  default-language: Haskell2010
+
+  -- build-tool-depends:
+  ghc-options:      -threaded -Wall
+
+executable initialise
+  main-is:          Main.hs
+  build-depends:
+    , base                  ^>=4.16.4.0 || ^>=4.17.0.0 || ^>=4.18.0.0
+    , filepath              ^>=1.4.2.2
+    , initialise-library
+    , optparse-applicative  ^>=0.18.1.0
+
+  hs-source-dirs:   bin/initialise
+
+  -- default-extensions:
+  -- other-extensions:
+  default-language: Haskell2010
+
+  -- build-tool-depends:
+  ghc-options:      -threaded -Wall
diff --git a/test/initialise/CabalGolden.hs b/test/initialise/CabalGolden.hs
new file mode 100644
--- /dev/null
+++ b/test/initialise/CabalGolden.hs
@@ -0,0 +1,46 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module CabalGolden (golden) where
+
+import qualified Cabal as SUT
+import Configuration (Configuration (..))
+import Control.Monad.Reader (liftIO)
+import Data.ByteString (readFile)
+import Data.ByteString.Lazy.Char8 (pack)
+import Data.Maybe (fromJust)
+import Distribution.SPDX.LicenseId (LicenseId (MIT))
+import Initialise (runInitialise)
+import Network.URI (parseURI)
+import System.FilePath (isExtensionOf, normalise, replaceExtension, takeBaseName)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.Golden (findByExtension, goldenVsStringDiff)
+import Prelude hiding (readFile)
+
+golden :: IO TestTree
+golden =
+  testGroup "Cabal.convert"
+    . map convertTest
+    . filter (not . (".golden.cabal" `isExtensionOf`))
+    <$> findByExtension [".cabal"] d
+  where
+    d = normalise "test/initialise/data"
+
+convertTest :: FilePath -> TestTree
+convertTest p = goldenVsStringDiff n diff gold action
+  where
+    n = takeBaseName p
+    diff a b = ["diff", "-u", a, b]
+    gold = p `replaceExtension` ".golden.cabal"
+    action = do
+      contents <- liftIO (readFile p)
+      pack <$> runInitialise (SUT.convert contents) configuration
+    configuration =
+      Configuration
+        { name = "sentinel",
+          homepage = fromJust (parseURI "https://github.com/sentinel/sentinel.git"),
+          author = "Sentinel",
+          maintainer = "sentinel@example.com",
+          licence = MIT,
+          path = ".",
+          year = 1970
+        }
diff --git a/test/initialise/ConfigurationSpec.hs b/test/initialise/ConfigurationSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/initialise/ConfigurationSpec.hs
@@ -0,0 +1,42 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module ConfigurationSpec (spec) where
+
+import qualified Configuration as SUT
+import Data.Maybe (fromJust)
+import Defaults (Defaults (..))
+import Network.URI (parseURI)
+import Options.Applicative (ParserResult (CompletionInvoked, Failure, Success), defaultPrefs, execParserPure, helper, info, renderFailure, (<**>))
+import System.Exit (ExitCode (ExitFailure))
+import Test.Hspec (Expectation, Spec, describe, expectationFailure, it, shouldBe)
+
+spec :: Spec
+spec =
+  describe "Configuration" $ do
+    describe "parser" $ do
+      it "should error if homepage isn't a URI" $
+        parse ["--homepage", "not-a-url"] `shouldFailWith` ("", ExitFailure 1)
+      it "should error if licence isn't an SPDX licence ID" $ do
+        parse ["--licence", "not-a-licence"] `shouldFailWith` ("", ExitFailure 1)
+
+parse :: [String] -> ParserResult SUT.Configuration
+parse =
+  execParserPure
+    defaultPrefs
+    ( info (SUT.parser defaults <**> helper) mempty
+    )
+
+shouldFailWith :: ParserResult SUT.Configuration -> (String, ExitCode) -> Expectation
+shouldFailWith (Success _) _ = expectationFailure "Expected Failure but got Success"
+shouldFailWith (CompletionInvoked _) _ = expectationFailure "Expected Failure but got CompletionInvoked"
+shouldFailWith (Failure f) rhs = renderFailure f "" `shouldBe` rhs
+
+defaults :: Defaults
+defaults =
+  Defaults
+    { dOrigin = fromJust (parseURI "http://github.com/username/repository.git"),
+      dAuthor = "Forename Surname",
+      dMaintainer = "username@example.com",
+      dPath = ".",
+      dYear = 1970
+    }
diff --git a/test/initialise/DefaultsSpec.hs b/test/initialise/DefaultsSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/initialise/DefaultsSpec.hs
@@ -0,0 +1,39 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module DefaultsSpec (spec) where
+
+import Data.Maybe (fromJust)
+import qualified Defaults as SUT
+import Hooks (withGitRepo)
+import Network.URI (parseURI)
+import Test.Hspec (Spec, around, describe, it, shouldBe)
+
+spec :: Spec
+spec = describe "Defaults" $ do
+  describe "dName" $ do
+    it "converts origin to project name" $
+      SUT.dName defaults `shouldBe` "repository"
+  describe "dHomePage" $ do
+    it "converts origin to a home page" $
+      SUT.dHomePage defaults `shouldBe` fromJust (parseURI "http://github.com/username/repository")
+  describe "getDefaults" $ around withGitRepo $ do
+    it "inspects the current repository" $ \p -> do
+      ds <- SUT.getDefaults
+      ds
+        `shouldBe` SUT.Defaults
+          { SUT.dOrigin = fromJust (parseURI "https://github.com/sentinel/sentinel.git"),
+            SUT.dAuthor = "Sentinel",
+            SUT.dMaintainer = "sentinel@example.com",
+            SUT.dPath = p,
+            SUT.dYear = 2023
+          }
+
+defaults :: SUT.Defaults
+defaults =
+  SUT.Defaults
+    { SUT.dOrigin = fromJust (parseURI "http://github.com/username/repository.git"),
+      SUT.dAuthor = "Forename Surname",
+      SUT.dMaintainer = "username@example.com",
+      SUT.dPath = ".",
+      SUT.dYear = 1970
+    }
diff --git a/test/initialise/FileGolden.hs b/test/initialise/FileGolden.hs
new file mode 100644
--- /dev/null
+++ b/test/initialise/FileGolden.hs
@@ -0,0 +1,56 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module FileGolden (golden) where
+
+import Configuration (Configuration (..))
+import Control.Monad.Reader (liftIO)
+import Data.Maybe (fromJust)
+import Data.Text.IO (readFile)
+import Data.Text.Lazy (fromStrict)
+import Data.Text.Lazy.Encoding (encodeUtf8)
+import Distribution.SPDX.LicenseId (LicenseId (MIT))
+import qualified File as SUT
+import Initialise (runInitialise)
+import Network.URI (parseURI)
+import System.FilePath (isExtensionOf, normalise, replaceExtension, takeBaseName, takeExtension)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.Golden (findByExtension, goldenVsStringDiff)
+import Prelude hiding (readFile)
+
+golden :: IO TestTree
+golden = do
+  testGroup "File.convert"
+    <$> mapM
+      g
+      [ "devcontainer.json",
+        "CHANGELOG.md",
+        "haskell-ci.yml"
+      ]
+  where
+    d = normalise "test/initialise/data"
+    g n =
+      let ext = takeExtension n
+       in testGroup n
+            . map convertTest
+            . filter (not . ((".golden" ++ ext) `isExtensionOf`))
+            <$> findByExtension [ext] d
+
+convertTest :: FilePath -> TestTree
+convertTest p = goldenVsStringDiff n diff gold action
+  where
+    n = takeBaseName p
+    diff a b = ["diff", "-u", a, b]
+    gold = p `replaceExtension` ".golden" ++ takeExtension p
+    action = do
+      contents <- liftIO (readFile p)
+      encodeUtf8 . fromStrict <$> runInitialise (SUT.convert contents) configuration
+    configuration =
+      Configuration
+        { name = "sentinel",
+          homepage = fromJust (parseURI "https://github.com/sentinel/sentinel.git"),
+          author = "Sentinel",
+          maintainer = "sentinel@example.com",
+          licence = MIT,
+          path = ".",
+          year = 1970
+        }
diff --git a/test/initialise/GitSpec.hs b/test/initialise/GitSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/initialise/GitSpec.hs
@@ -0,0 +1,20 @@
+module GitSpec (spec) where
+
+import Data.Text (pack)
+import qualified Git as SUT
+import Hooks (withGitRepo)
+import Test.Hspec (Spec, around, describe, it, shouldBe)
+
+spec :: Spec
+spec = around withGitRepo $
+  describe "Git" $ do
+    describe "config" $ do
+      it "gets remote.origin.url" $ const $ do
+        origin <- SUT.config "remote.origin.url"
+        origin `shouldBe` pack "https://github.com/sentinel/sentinel.git"
+      it "gets user.name" $ const $ do
+        name <- SUT.config "user.name"
+        name `shouldBe` pack "Sentinel"
+      it "get user.email" $ const $ do
+        email <- SUT.config "user.email"
+        email `shouldBe` pack "sentinel@example.com"
diff --git a/test/initialise/Hooks.hs b/test/initialise/Hooks.hs
new file mode 100644
--- /dev/null
+++ b/test/initialise/Hooks.hs
@@ -0,0 +1,16 @@
+module Hooks (withGitRepo) where
+
+import Control.Monad (void)
+import System.Directory (withCurrentDirectory)
+import System.IO.Temp (withSystemTempDirectory)
+import System.Process (readProcess)
+
+withGitRepo :: (FilePath -> IO ()) -> IO ()
+withGitRepo action =
+  withSystemTempDirectory "initialise" $ \p ->
+    withCurrentDirectory p $ do
+      void $ readProcess "git" ["init"] ""
+      void $ readProcess "git" ["config", "user.name", "Sentinel"] ""
+      void $ readProcess "git" ["config", "user.email", "sentinel@example.com"] ""
+      void $ readProcess "git" ["remote", "add", "origin", "https://github.com/sentinel/sentinel.git"] ""
+      action p
diff --git a/test/initialise/Main.hs b/test/initialise/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/initialise/Main.hs
@@ -0,0 +1,25 @@
+module Main (main) where
+
+import qualified CabalGolden (golden)
+import qualified DefaultsSpec (spec)
+import qualified FileGolden (golden)
+import qualified GitSpec (spec)
+import Test.Tasty (defaultMain, testGroup)
+import Test.Tasty.Hspec (testSpecs)
+
+main :: IO ()
+main = do
+  specs <-
+    concat
+      <$> mapM
+        testSpecs
+        [ DefaultsSpec.spec,
+          GitSpec.spec
+        ]
+  goldens <- sequence [CabalGolden.golden, FileGolden.golden]
+  defaultMain $
+    testGroup
+      "initialise-library"
+      [ testGroup "Specs" specs,
+        testGroup "Golden" goldens
+      ]
diff --git a/test/initialise/data/CHANGELOG-20230912.golden.md b/test/initialise/data/CHANGELOG-20230912.golden.md
new file mode 100644
--- /dev/null
+++ b/test/initialise/data/CHANGELOG-20230912.golden.md
@@ -0,0 +1,5 @@
+# Revision history for sentinel
+
+## 0.1.0.0 -- YYYY-mm-dd
+
+* First version. Released on an unsuspecting world.
diff --git a/test/initialise/data/CHANGELOG-20230912.md b/test/initialise/data/CHANGELOG-20230912.md
new file mode 100644
--- /dev/null
+++ b/test/initialise/data/CHANGELOG-20230912.md
@@ -0,0 +1,5 @@
+# Revision history for template-hs
+
+## 0.1.0.0 -- YYYY-mm-dd
+
+* First version. Released on an unsuspecting world.
diff --git a/test/initialise/data/devcontainer-20230912.golden.json b/test/initialise/data/devcontainer-20230912.golden.json
new file mode 100644
--- /dev/null
+++ b/test/initialise/data/devcontainer-20230912.golden.json
@@ -0,0 +1,36 @@
+{
+  "name": "sentinel",
+  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
+  // Features to add to the dev container. More info: https://containers.dev/features.
+  "features": {
+    "ghcr.io/devcontainers-contrib/features/haskell:2": {
+      "globalPackages": "cabal-fmt haskell-ci hlint ormolu"
+    },
+    "ghcr.io/devcontainers-contrib/features/pre-commit:2": {},
+    "ghcr.io/devcontainers/features/git:1": {},
+    "ghcr.io/devcontainers/features/github-cli:1": {}
+  },
+  // Use 'forwardPorts' to make a list of ports inside the container available locally.
+  // "forwardPorts": [],
+  // Use 'postCreateCommand' to run commands after the container is created.
+  // "postCreateCommand": "",
+  // Configure tool-specific properties.
+  "customizations": {
+    "vscode": {
+      "settings": {
+        "haskell.manageHLS": "GHCup"
+      },
+      "extensions": [
+        "berberman.vscode-cabal-fmt",
+        "DavidAnson.vscode-markdownlint",
+        "eamodio.gitlens",
+        "github.vscode-github-actions",
+        "GitHub.vscode-pull-request-github",
+        "haskell.haskell",
+        "ms-azuretools.vscode-docker"
+      ]
+    }
+  },
+  // Comment out to connect as root instead. To add a non-root user, see: https://aka.ms/vscode-remote/containers/non-root.
+  "remoteUser": "vscode"
+}
diff --git a/test/initialise/data/devcontainer-20230912.json b/test/initialise/data/devcontainer-20230912.json
new file mode 100644
--- /dev/null
+++ b/test/initialise/data/devcontainer-20230912.json
@@ -0,0 +1,36 @@
+{
+  "name": "template.hs",
+  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
+  // Features to add to the dev container. More info: https://containers.dev/features.
+  "features": {
+    "ghcr.io/devcontainers-contrib/features/haskell:2": {
+      "globalPackages": "cabal-fmt haskell-ci hlint ormolu"
+    },
+    "ghcr.io/devcontainers-contrib/features/pre-commit:2": {},
+    "ghcr.io/devcontainers/features/git:1": {},
+    "ghcr.io/devcontainers/features/github-cli:1": {}
+  },
+  // Use 'forwardPorts' to make a list of ports inside the container available locally.
+  // "forwardPorts": [],
+  // Use 'postCreateCommand' to run commands after the container is created.
+  // "postCreateCommand": "",
+  // Configure tool-specific properties.
+  "customizations": {
+    "vscode": {
+      "settings": {
+        "haskell.manageHLS": "GHCup"
+      },
+      "extensions": [
+        "berberman.vscode-cabal-fmt",
+        "DavidAnson.vscode-markdownlint",
+        "eamodio.gitlens",
+        "github.vscode-github-actions",
+        "GitHub.vscode-pull-request-github",
+        "haskell.haskell",
+        "ms-azuretools.vscode-docker"
+      ]
+    }
+  },
+  // Comment out to connect as root instead. To add a non-root user, see: https://aka.ms/vscode-remote/containers/non-root.
+  "remoteUser": "vscode"
+}
diff --git a/test/initialise/data/haskell-ci-20230912.golden.yml b/test/initialise/data/haskell-ci-20230912.golden.yml
new file mode 100644
--- /dev/null
+++ b/test/initialise/data/haskell-ci-20230912.golden.yml
@@ -0,0 +1,199 @@
+# This GitHub workflow config has been generated by a script via
+#
+#   haskell-ci 'github' '--haddock' 'sentinel.cabal'
+#
+# To regenerate the script (for example after adjusting tested-with) run
+#
+#   haskell-ci regenerate
+#
+# For more information, see https://github.com/haskell-CI/haskell-ci
+#
+# version: 0.16.6
+#
+# REGENDATA ("0.16.6",["github","--haddock","sentinel.cabal"])
+#
+name: Haskell-CI
+on:
+  - push
+  - pull_request
+jobs:
+  linux:
+    name: Haskell-CI - Linux - ${{ matrix.compiler }}
+    runs-on: ubuntu-20.04
+    timeout-minutes:
+      60
+    container:
+      image: buildpack-deps:bionic
+    continue-on-error: ${{ matrix.allow-failure }}
+    strategy:
+      matrix:
+        include:
+          - compiler: ghc-9.6.2
+            compilerKind: ghc
+            compilerVersion: 9.6.2
+            setup-method: ghcup
+            allow-failure: false
+          - compiler: ghc-9.4.5
+            compilerKind: ghc
+            compilerVersion: 9.4.5
+            setup-method: ghcup
+            allow-failure: false
+          - compiler: ghc-9.2.8
+            compilerKind: ghc
+            compilerVersion: 9.2.8
+            setup-method: ghcup
+            allow-failure: false
+      fail-fast: false
+    steps:
+      - name: apt
+        run: |
+          apt-get update
+          apt-get install -y --no-install-recommends gnupg ca-certificates dirmngr curl git software-properties-common libtinfo5
+          mkdir -p "$HOME/.ghcup/bin"
+          curl -sL https://downloads.haskell.org/ghcup/0.1.19.2/x86_64-linux-ghcup-0.1.19.2 > "$HOME/.ghcup/bin/ghcup"
+          chmod a+x "$HOME/.ghcup/bin/ghcup"
+          "$HOME/.ghcup/bin/ghcup" install ghc "$HCVER" || (cat "$HOME"/.ghcup/logs/*.* && false)
+          "$HOME/.ghcup/bin/ghcup" install cabal 3.10.1.0 || (cat "$HOME"/.ghcup/logs/*.* && false)
+        env:
+          HCKIND: ${{ matrix.compilerKind }}
+          HCNAME: ${{ matrix.compiler }}
+          HCVER: ${{ matrix.compilerVersion }}
+      - name: Set PATH and environment variables
+        run: |
+          echo "$HOME/.cabal/bin" >> $GITHUB_PATH
+          echo "LANG=C.UTF-8" >> "$GITHUB_ENV"
+          echo "CABAL_DIR=$HOME/.cabal" >> "$GITHUB_ENV"
+          echo "CABAL_CONFIG=$HOME/.cabal/config" >> "$GITHUB_ENV"
+          HCDIR=/opt/$HCKIND/$HCVER
+          HC=$HOME/.ghcup/bin/$HCKIND-$HCVER
+          echo "HC=$HC" >> "$GITHUB_ENV"
+          echo "HCPKG=$HOME/.ghcup/bin/$HCKIND-pkg-$HCVER" >> "$GITHUB_ENV"
+          echo "HADDOCK=$HOME/.ghcup/bin/haddock-$HCVER" >> "$GITHUB_ENV"
+          echo "CABAL=$HOME/.ghcup/bin/cabal-3.10.1.0 -vnormal+nowrap" >> "$GITHUB_ENV"
+          HCNUMVER=$(${HC} --numeric-version|perl -ne '/^(\d+)\.(\d+)\.(\d+)(\.(\d+))?$/; print(10000 * $1 + 100 * $2 + ($3 == 0 ? $5 != 1 : $3))')
+          echo "HCNUMVER=$HCNUMVER" >> "$GITHUB_ENV"
+          echo "ARG_TESTS=--enable-tests" >> "$GITHUB_ENV"
+          echo "ARG_BENCH=--enable-benchmarks" >> "$GITHUB_ENV"
+          echo "HEADHACKAGE=false" >> "$GITHUB_ENV"
+          echo "ARG_COMPILER=--$HCKIND --with-compiler=$HC" >> "$GITHUB_ENV"
+          echo "GHCJSARITH=0" >> "$GITHUB_ENV"
+        env:
+          HCKIND: ${{ matrix.compilerKind }}
+          HCNAME: ${{ matrix.compiler }}
+          HCVER: ${{ matrix.compilerVersion }}
+      - name: env
+        run: |
+          env
+      - name: write cabal config
+        run: |
+          mkdir -p $CABAL_DIR
+          cat >> $CABAL_CONFIG <<EOF
+          remote-build-reporting: anonymous
+          write-ghc-environment-files: never
+          remote-repo-cache: $CABAL_DIR/packages
+          logs-dir:          $CABAL_DIR/logs
+          world-file:        $CABAL_DIR/world
+          extra-prog-path:   $CABAL_DIR/bin
+          symlink-bindir:    $CABAL_DIR/bin
+          installdir:        $CABAL_DIR/bin
+          build-summary:     $CABAL_DIR/logs/build.log
+          store-dir:         $CABAL_DIR/store
+          install-dirs user
+            prefix: $CABAL_DIR
+          repository hackage.haskell.org
+            url: http://hackage.haskell.org/
+          EOF
+          cat >> $CABAL_CONFIG <<EOF
+          program-default-options
+            ghc-options: $GHCJOBS +RTS -M3G -RTS
+          EOF
+          cat $CABAL_CONFIG
+      - name: versions
+        run: |
+          $HC --version || true
+          $HC --print-project-git-commit-id || true
+          $CABAL --version || true
+      - name: update cabal index
+        run: |
+          $CABAL v2-update -v
+      - name: install cabal-plan
+        run: |
+          mkdir -p $HOME/.cabal/bin
+          curl -sL https://github.com/haskell-hvr/cabal-plan/releases/download/v0.7.3.0/cabal-plan-0.7.3.0-x86_64-linux.xz > cabal-plan.xz
+          echo 'f62ccb2971567a5f638f2005ad3173dba14693a45154c1508645c52289714cb2  cabal-plan.xz' | sha256sum -c -
+          xz -d < cabal-plan.xz > $HOME/.cabal/bin/cabal-plan
+          rm -f cabal-plan.xz
+          chmod a+x $HOME/.cabal/bin/cabal-plan
+          cabal-plan --version
+      - name: checkout
+        uses: actions/checkout@v3
+        with:
+          path: source
+      - name: initial cabal.project for sdist
+        run: |
+          touch cabal.project
+          echo "packages: $GITHUB_WORKSPACE/source/." >> cabal.project
+          cat cabal.project
+      - name: sdist
+        run: |
+          mkdir -p sdist
+          $CABAL sdist all --output-dir $GITHUB_WORKSPACE/sdist
+      - name: unpack
+        run: |
+          mkdir -p unpacked
+          find sdist -maxdepth 1 -type f -name '*.tar.gz' -exec tar -C $GITHUB_WORKSPACE/unpacked -xzvf {} \;
+      - name: generate cabal.project
+        run: |
+          PKGDIR_sentinel="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/sentinel-[0-9.]*')"
+          echo "PKGDIR_sentinel=${PKGDIR_sentinel}" >> "$GITHUB_ENV"
+          rm -f cabal.project cabal.project.local
+          touch cabal.project
+          touch cabal.project.local
+          echo "packages: ${PKGDIR_sentinel}" >> cabal.project
+          echo "package sentinel" >> cabal.project
+          echo "    ghc-options: -Werror=missing-methods" >> cabal.project
+          cat >> cabal.project <<EOF
+          EOF
+          $HCPKG list --simple-output --names-only | perl -ne 'for (split /\s+/) { print "constraints: $_ installed\n" unless /^(sentinel)$/; }' >> cabal.project.local
+          cat cabal.project
+          cat cabal.project.local
+      - name: dump install plan
+        run: |
+          $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dry-run all
+          cabal-plan
+      - name: restore cache
+        uses: actions/cache/restore@v3
+        with:
+          key: ${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }}
+          path: ~/.cabal/store
+          restore-keys: ${{ runner.os }}-${{ matrix.compiler }}-
+      - name: install dependencies
+        run: |
+          $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks --dependencies-only -j2 all
+          $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dependencies-only -j2 all
+      - name: build w/o tests
+        run: |
+          $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all
+      - name: build
+        run: |
+          $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --write-ghc-environment-files=always
+      - name: tests
+        run: |
+          $CABAL v2-test $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --test-show-details=direct
+      - name: cabal check
+        run: |
+          cd ${PKGDIR_sentinel} || false
+          ${CABAL} -vnormal check
+      - name: haddock
+        run: |
+          $CABAL v2-haddock --disable-documentation --haddock-all $ARG_COMPILER --with-haddock $HADDOCK $ARG_TESTS $ARG_BENCH all
+      - name: unconstrained build
+        run: |
+          rm -f cabal.project.local
+          $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all
+      - name: save cache
+        uses: actions/cache/save@v3
+        if: always()
+        with:
+          key: ${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }}
+          path: ~/.cabal/store
diff --git a/test/initialise/data/haskell-ci-20230912.yml b/test/initialise/data/haskell-ci-20230912.yml
new file mode 100644
--- /dev/null
+++ b/test/initialise/data/haskell-ci-20230912.yml
@@ -0,0 +1,199 @@
+# This GitHub workflow config has been generated by a script via
+#
+#   haskell-ci 'github' '--haddock' 'templatise.cabal'
+#
+# To regenerate the script (for example after adjusting tested-with) run
+#
+#   haskell-ci regenerate
+#
+# For more information, see https://github.com/haskell-CI/haskell-ci
+#
+# version: 0.16.6
+#
+# REGENDATA ("0.16.6",["github","--haddock","templatise.cabal"])
+#
+name: Haskell-CI
+on:
+  - push
+  - pull_request
+jobs:
+  linux:
+    name: Haskell-CI - Linux - ${{ matrix.compiler }}
+    runs-on: ubuntu-20.04
+    timeout-minutes:
+      60
+    container:
+      image: buildpack-deps:bionic
+    continue-on-error: ${{ matrix.allow-failure }}
+    strategy:
+      matrix:
+        include:
+          - compiler: ghc-9.6.2
+            compilerKind: ghc
+            compilerVersion: 9.6.2
+            setup-method: ghcup
+            allow-failure: false
+          - compiler: ghc-9.4.5
+            compilerKind: ghc
+            compilerVersion: 9.4.5
+            setup-method: ghcup
+            allow-failure: false
+          - compiler: ghc-9.2.8
+            compilerKind: ghc
+            compilerVersion: 9.2.8
+            setup-method: ghcup
+            allow-failure: false
+      fail-fast: false
+    steps:
+      - name: apt
+        run: |
+          apt-get update
+          apt-get install -y --no-install-recommends gnupg ca-certificates dirmngr curl git software-properties-common libtinfo5
+          mkdir -p "$HOME/.ghcup/bin"
+          curl -sL https://downloads.haskell.org/ghcup/0.1.19.2/x86_64-linux-ghcup-0.1.19.2 > "$HOME/.ghcup/bin/ghcup"
+          chmod a+x "$HOME/.ghcup/bin/ghcup"
+          "$HOME/.ghcup/bin/ghcup" install ghc "$HCVER" || (cat "$HOME"/.ghcup/logs/*.* && false)
+          "$HOME/.ghcup/bin/ghcup" install cabal 3.10.1.0 || (cat "$HOME"/.ghcup/logs/*.* && false)
+        env:
+          HCKIND: ${{ matrix.compilerKind }}
+          HCNAME: ${{ matrix.compiler }}
+          HCVER: ${{ matrix.compilerVersion }}
+      - name: Set PATH and environment variables
+        run: |
+          echo "$HOME/.cabal/bin" >> $GITHUB_PATH
+          echo "LANG=C.UTF-8" >> "$GITHUB_ENV"
+          echo "CABAL_DIR=$HOME/.cabal" >> "$GITHUB_ENV"
+          echo "CABAL_CONFIG=$HOME/.cabal/config" >> "$GITHUB_ENV"
+          HCDIR=/opt/$HCKIND/$HCVER
+          HC=$HOME/.ghcup/bin/$HCKIND-$HCVER
+          echo "HC=$HC" >> "$GITHUB_ENV"
+          echo "HCPKG=$HOME/.ghcup/bin/$HCKIND-pkg-$HCVER" >> "$GITHUB_ENV"
+          echo "HADDOCK=$HOME/.ghcup/bin/haddock-$HCVER" >> "$GITHUB_ENV"
+          echo "CABAL=$HOME/.ghcup/bin/cabal-3.10.1.0 -vnormal+nowrap" >> "$GITHUB_ENV"
+          HCNUMVER=$(${HC} --numeric-version|perl -ne '/^(\d+)\.(\d+)\.(\d+)(\.(\d+))?$/; print(10000 * $1 + 100 * $2 + ($3 == 0 ? $5 != 1 : $3))')
+          echo "HCNUMVER=$HCNUMVER" >> "$GITHUB_ENV"
+          echo "ARG_TESTS=--enable-tests" >> "$GITHUB_ENV"
+          echo "ARG_BENCH=--enable-benchmarks" >> "$GITHUB_ENV"
+          echo "HEADHACKAGE=false" >> "$GITHUB_ENV"
+          echo "ARG_COMPILER=--$HCKIND --with-compiler=$HC" >> "$GITHUB_ENV"
+          echo "GHCJSARITH=0" >> "$GITHUB_ENV"
+        env:
+          HCKIND: ${{ matrix.compilerKind }}
+          HCNAME: ${{ matrix.compiler }}
+          HCVER: ${{ matrix.compilerVersion }}
+      - name: env
+        run: |
+          env
+      - name: write cabal config
+        run: |
+          mkdir -p $CABAL_DIR
+          cat >> $CABAL_CONFIG <<EOF
+          remote-build-reporting: anonymous
+          write-ghc-environment-files: never
+          remote-repo-cache: $CABAL_DIR/packages
+          logs-dir:          $CABAL_DIR/logs
+          world-file:        $CABAL_DIR/world
+          extra-prog-path:   $CABAL_DIR/bin
+          symlink-bindir:    $CABAL_DIR/bin
+          installdir:        $CABAL_DIR/bin
+          build-summary:     $CABAL_DIR/logs/build.log
+          store-dir:         $CABAL_DIR/store
+          install-dirs user
+            prefix: $CABAL_DIR
+          repository hackage.haskell.org
+            url: http://hackage.haskell.org/
+          EOF
+          cat >> $CABAL_CONFIG <<EOF
+          program-default-options
+            ghc-options: $GHCJOBS +RTS -M3G -RTS
+          EOF
+          cat $CABAL_CONFIG
+      - name: versions
+        run: |
+          $HC --version || true
+          $HC --print-project-git-commit-id || true
+          $CABAL --version || true
+      - name: update cabal index
+        run: |
+          $CABAL v2-update -v
+      - name: install cabal-plan
+        run: |
+          mkdir -p $HOME/.cabal/bin
+          curl -sL https://github.com/haskell-hvr/cabal-plan/releases/download/v0.7.3.0/cabal-plan-0.7.3.0-x86_64-linux.xz > cabal-plan.xz
+          echo 'f62ccb2971567a5f638f2005ad3173dba14693a45154c1508645c52289714cb2  cabal-plan.xz' | sha256sum -c -
+          xz -d < cabal-plan.xz > $HOME/.cabal/bin/cabal-plan
+          rm -f cabal-plan.xz
+          chmod a+x $HOME/.cabal/bin/cabal-plan
+          cabal-plan --version
+      - name: checkout
+        uses: actions/checkout@v3
+        with:
+          path: source
+      - name: initial cabal.project for sdist
+        run: |
+          touch cabal.project
+          echo "packages: $GITHUB_WORKSPACE/source/." >> cabal.project
+          cat cabal.project
+      - name: sdist
+        run: |
+          mkdir -p sdist
+          $CABAL sdist all --output-dir $GITHUB_WORKSPACE/sdist
+      - name: unpack
+        run: |
+          mkdir -p unpacked
+          find sdist -maxdepth 1 -type f -name '*.tar.gz' -exec tar -C $GITHUB_WORKSPACE/unpacked -xzvf {} \;
+      - name: generate cabal.project
+        run: |
+          PKGDIR_templatise="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/templatise-[0-9.]*')"
+          echo "PKGDIR_templatise=${PKGDIR_templatise}" >> "$GITHUB_ENV"
+          rm -f cabal.project cabal.project.local
+          touch cabal.project
+          touch cabal.project.local
+          echo "packages: ${PKGDIR_templatise}" >> cabal.project
+          echo "package templatise" >> cabal.project
+          echo "    ghc-options: -Werror=missing-methods" >> cabal.project
+          cat >> cabal.project <<EOF
+          EOF
+          $HCPKG list --simple-output --names-only | perl -ne 'for (split /\s+/) { print "constraints: $_ installed\n" unless /^(templatise)$/; }' >> cabal.project.local
+          cat cabal.project
+          cat cabal.project.local
+      - name: dump install plan
+        run: |
+          $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dry-run all
+          cabal-plan
+      - name: restore cache
+        uses: actions/cache/restore@v3
+        with:
+          key: ${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }}
+          path: ~/.cabal/store
+          restore-keys: ${{ runner.os }}-${{ matrix.compiler }}-
+      - name: install dependencies
+        run: |
+          $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks --dependencies-only -j2 all
+          $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dependencies-only -j2 all
+      - name: build w/o tests
+        run: |
+          $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all
+      - name: build
+        run: |
+          $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --write-ghc-environment-files=always
+      - name: tests
+        run: |
+          $CABAL v2-test $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --test-show-details=direct
+      - name: cabal check
+        run: |
+          cd ${PKGDIR_templatise} || false
+          ${CABAL} -vnormal check
+      - name: haddock
+        run: |
+          $CABAL v2-haddock --disable-documentation --haddock-all $ARG_COMPILER --with-haddock $HADDOCK $ARG_TESTS $ARG_BENCH all
+      - name: unconstrained build
+        run: |
+          rm -f cabal.project.local
+          $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all
+      - name: save cache
+        uses: actions/cache/save@v3
+        if: always()
+        with:
+          key: ${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }}
+          path: ~/.cabal/store
diff --git a/test/initialise/data/templatise-20230829.cabal b/test/initialise/data/templatise-20230829.cabal
new file mode 100644
--- /dev/null
+++ b/test/initialise/data/templatise-20230829.cabal
@@ -0,0 +1,116 @@
+cabal-version:      3.0
+name:               templatise
+version:            0.1.0.0
+license:            Unlicense
+license-file:       LICENSE
+copyright:          (c) 2023 Alex Brandt
+author:             Alex Brandt
+maintainer:         alunduil@gmail.com
+stability:          alpha
+homepage:           https://github.com/alunduil/template.hs
+bug-reports:        https://github.com/alunduil/template.hs/issues
+synopsis:
+  You can use template.hs to create a new Haskell GitHub repository.
+
+description:
+  You can use template.hs to create a new GitHub repository. The repository will
+  have Haskell, VS Code devcontainers, and various GitHub actions ready to use.
+
+category:           VSCode
+tested-with:        GHC ==9.2.8 || ==9.4.5 || ==9.6.2
+extra-source-files:
+  CHANGELOG.md
+  README.md
+
+source-repository head
+  type:     git
+  location: https://github.com/alunduil/template.hs
+
+common initialise-common
+  build-depends:
+    , base                  ^>=4.16.4.0 || ^>=4.17.0.0 || ^>=4.18.0.0
+    , bytestring            ^>=0.11.4.0 || ^>=0.12.0.2
+    , Cabal-syntax          ^>=3.10.1.0
+    , filepath              ^>=1.4.2.2
+    , mtl                   ^>=2.2.2    || ^>=2.3.1
+    , network-uri           ^>=2.6.4.1  || ^>=2.7.0.0
+    , optparse-applicative  ^>=0.18.1.0
+    , text                  ^>=2.0.2
+
+library initialise-library
+  import:           initialise-common
+  exposed-modules:
+    Cabal
+    Configuration
+    Defaults
+    Git
+    Initialise
+
+  -- TODO Make Git private.
+  --other-modules: Git
+  build-depends:
+    , exceptions  ^>=0.10.5
+    , extra       ^>=1.7.14
+    , parsec      ^>=3.1.16.1
+    , process     ^>=1.6.16.0
+    , time        ^>=1.11.1.1 || ^>=1.12.2
+
+  hs-source-dirs:   lib/initialise
+
+  --default-extensions:
+  --other-extensions:
+  default-language: Haskell2010
+
+  --build-tool-depends:
+  ghc-options:      -Wall
+
+test-suite initialise-test
+  import:           initialise-common
+  type:             exitcode-stdio-1.0
+  main-is:          Main.hs
+  build-depends:
+    , hspec               ^>=2.11.4
+    , initialise-library
+    , tasty               ^>=1.4.3
+    , tasty-golden        ^>=2.3.5
+    , tasty-hspec         ^>=1.2.0.4
+
+  other-modules:
+    CabalGolden
+    ConfigurationSpec
+    DefaultsSpec
+    GitSpec
+
+  hs-source-dirs:   test/initialise
+
+  --default-extensions:
+  -- other-extensions:
+  default-language: Haskell2010
+
+  -- build-tool-depends:
+  ghc-options:      -threaded -Wall
+
+executable initialise
+  main-is:          Main.hs
+  build-depends:
+    , base                ^>=4.16.4.0 || ^>=4.17.0.0 || ^>=4.18.0.0
+    , http-client         ^>=0.7.13.1
+    , http-conduit        ^>=2.3.8.3
+    , initialise-library
+
+  --other-modules:
+  --  Cabal
+  --  Configuration
+  --  File
+  --  Initialise
+  --  Licence
+  --  Replace
+
+  hs-source-dirs:   bin/initialise
+
+  -- default-extensions:
+  -- other-extensions:
+  default-language: Haskell2010
+
+  -- build-tool-depends:
+  ghc-options:      -threaded -Wall
diff --git a/test/initialise/data/templatise-20230829.golden.cabal b/test/initialise/data/templatise-20230829.golden.cabal
new file mode 100644
--- /dev/null
+++ b/test/initialise/data/templatise-20230829.golden.cabal
@@ -0,0 +1,75 @@
+cabal-version:      3.0
+name:               sentinel
+version:            0.1.0.0
+license:            MIT
+license-file:       LICENSE
+copyright:          (c) 1970 Sentinel
+author:             Sentinel
+maintainer:         sentinel@example.com
+stability:          alpha
+homepage:           https://github.com/sentinel/sentinel.git
+bug-reports:        https://github.com/sentinel/sentinel.git/issues
+synopsis:           TODO
+description:        TODO
+category:           VSCode
+tested-with:        GHC ==9.2.8 || ==9.4.5 || ==9.6.2
+extra-source-files:
+    CHANGELOG.md
+    README.md
+
+source-repository head
+    type:     git
+    location: https://github.com/sentinel/sentinel.git
+
+common sentinel-common
+    build-depends:
+        , base                  ^>=4.16.4.0 || ^>=4.17.0.0 || ^>=4.18.0.0
+        , bytestring            ^>=0.11.4.0 || ^>=0.12.0.2
+        , Cabal-syntax          ^>=3.10.1.0
+        , filepath              ^>=1.4.2.2
+        , mtl                   ^>=2.2.2    || ^>=2.3.1
+        , network-uri           ^>=2.6.4.1  || ^>=2.7.0.0
+        , optparse-applicative  ^>=0.18.1.0
+        , text                  ^>=2.0.2
+
+library sentinel-library
+    import:           sentinel-common
+    exposed-modules:
+    build-depends:
+        , exceptions  ^>=0.10.5
+        , extra       ^>=1.7.14
+        , parsec      ^>=3.1.16.1
+        , process     ^>=1.6.16.0
+        , time        ^>=1.11.1.1 || ^>=1.12.2
+
+    hs-source-dirs:   lib/sentinel
+    default-language: Haskell2010
+    ghc-options:      -Wall
+
+test-suite sentinel-test
+    import:           sentinel-common
+    type:             exitcode-stdio-1.0
+    main-is:          Main.hs
+    build-depends:
+        , hspec               ^>=2.11.4
+        , sentinel-library
+        , tasty               ^>=1.4.3
+        , tasty-golden        ^>=2.3.5
+        , tasty-hspec         ^>=1.2.0.4
+
+    other-modules:
+    hs-source-dirs:   test/sentinel
+    default-language: Haskell2010
+    ghc-options:      -threaded -Wall
+
+executable sentinel
+    main-is:          Main.hs
+    build-depends:
+        , base                ^>=4.16.4.0 || ^>=4.17.0.0 || ^>=4.18.0.0
+        , http-client         ^>=0.7.13.1
+        , http-conduit        ^>=2.3.8.3
+        , sentinel-library
+
+    hs-source-dirs:   bin/sentinel
+    default-language: Haskell2010
+    ghc-options:      -threaded -Wall
