packages feed

hwm-0.1.1: src/HWM/Runtime/Network.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NoImplicitPrelude #-}

module HWM.Runtime.Network (uploadToGitHub, getGHUploadUrl) where

import Control.Monad.Except (MonadError (..))
import Data.Aeson (FromJSON)
import qualified Data.Text as T
import HWM.Core.Result (Issue (..))
import HWM.Domain.Config (Config (..))
import Network.HTTP.Req
import Relude
import System.FilePath (takeFileName)
import Text.URI (mkURI)

getGitHubToken :: (MonadIO m, MonadError Issue m) => m Text
getGitHubToken = do
  maybeToken <- liftIO $ lookupEnv "GITHUB_TOKEN"
  maybe
    (throwError "GITHUB_TOKEN environment variable not set. Please set it to a valid GitHub Personal Access Token with repo permissions.")
    (pure . T.pack)
    maybeToken

uploadToGitHub :: (MonadIO m, MonadError Issue m) => Text -> FilePath -> m ()
uploadToGitHub uploadUrl filePath = do
  token <- getGitHubToken
  liftIO $ runReq defaultHttpConfig $ do
    uri <- liftIO $ mkURI uploadUrl
    case useHttpsURI uri of
      Just (url, opts) -> do
        let fileName = T.pack $ takeFileName filePath
        void
          $ req
            POST
            url
            (ReqBodyFile filePath) -- Raw bytes, no multipart wrapping
            ignoreResponse
            ( opts
                <> queryParam "name" (Just fileName) -- Required by GitHub
                <> header "Authorization" ("Bearer " <> encodeUtf8 token)
                <> header "Content-Type" "application/octet-stream" -- Crucial!
                <> header "User-Agent" "hwm-tool" -- GitHub requires a User-Agent
            )
      Nothing -> liftIO $ putStrLn "GitHub Upload URLs must be HTTPS"

-- 1. Define a tiny data type to represent the GitHub JSON response.
-- Aeson automatically maps the "upload_url" JSON key to this record field.

data GitHubRelease = GitHubRelease
  { name :: Text,
    upload_url :: Text
  }
  deriving (Show, Generic)

-- 2. Automatically derive the JSON parser
instance FromJSON GitHubRelease

-- 3. The main function returning the clean URL
getGHUploadUrl :: (MonadIO m, MonadError Issue m) => Config -> Text -> m Text
getGHUploadUrl Config {..} tag = do
  gh <- maybe (throwError "GitHub repository not configured") pure cfgGithub
  token <- getGitHubToken
  liftIO $ runReq defaultHttpConfig $ do
    -- Construct the endpoint URL
    let urlStr = "https://api.github.com/repos/" <> gh <> "/releases/tags/" <> tag
    uri <- liftIO $ mkURI urlStr
    case useHttpsURI uri of
      Just (url, opts) -> do
        -- Execute the GET request, expecting a JSON response matching our GitHubRelease type
        r <-
          req
            GET
            url
            NoReqBody
            jsonResponse -- This automatically parses the ByteString into our GitHubRelease data type!
            ( opts
                <> header "Authorization" ("Bearer " <> encodeUtf8 token)
                <> header "Accept" "application/vnd.github+json"
                <> header "User-Agent" "hwm-tool"
            )

        -- Extract the raw URL from the parsed JSON object
        let rawUrl = upload_url (responseBody r)

        -- Strip the "{?name,label}" template suffix before returning
        return $ T.takeWhile (/= '{') rawUrl
      Nothing -> error "GitHub API URLs must be HTTPS"