Skip to content

Instantly share code, notes, and snippets.

@AphonicChaos
Last active November 7, 2022 00:37
Show Gist options
  • Save AphonicChaos/b27b1326fada1506f8f457286e8df5db to your computer and use it in GitHub Desktop.
Save AphonicChaos/b27b1326fada1506f8f457286e8df5db to your computer and use it in GitHub Desktop.
Build and release binaries for a Haskell app on Windows, MacOS X, and Linux (x86_64, sorry Apple Silicon)
name: CI
on:
pull_request:
push:
branches:
- main
workflow_call:
outputs:
version:
value: ${{ jobs.build_prod.outputs.version }}
jobs:
cabal_test:
strategy:
matrix:
ghc:
- 9.2.4
cabal:
- 3.6.2.0
name: 'cabal_test: ghc-${{ matrix.ghc }}'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: haskell/actions/setup@v2
with:
ghc-version: ${{ matrix.ghc }}
cabal-version: ${{ matrix.cabal }}
- uses: actions/cache@v3
with:
path: ~/.cabal
key: ${{ runner.os }}-cabal-cache-${{ matrix.ghc }}-${{ matrix.cabal }}-${{ hashFiles('log-parser.cabal') }}
- run: cabal update
- run: cabal test
os_test:
strategy:
matrix:
os:
- windows-latest
- ubuntu-latest
- macos-latest
name: 'os_test: ${{ matrix.os }}'
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v3
with:
path: ~/.cabal
key: ${{ runner.os }}-os_test-${{ hashFiles('log-parser.cabal') }}
- run: cabal test --disable-optimization
build_prod:
defaults:
run:
shell: bash
strategy:
matrix:
os:
- windows-latest
- ubuntu-latest
- macos-latest
name: 'build_prod: ${{ matrix.os }}'
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v3
with:
path: ~/.cabal
key: ${{ runner.os }}-build_prod-${{ hashFiles('log-parser.cabal') }}
- name: Build
run: cabal install --install-method=copy --overwrite-policy=always --installdir=./bin/
- name: Get build info
run: scripts/GetBuildInfo.hs >> "${GITHUB_OUTPUT}"
id: build-info
- name: Rename binary
run: |
cp bin/log-parser$ext bin/log-parser-$version-$os-$arch$ext
env:
version: ${{ steps.build-info.outputs.version }}
os: ${{ steps.build-info.outputs.os }}
arch: ${{ steps.build-info.outputs.arch }}
ext: ${{ steps.build-info.outputs.ext }}
- name: Store binary
uses: actions/upload-artifact@v3
with:
name: log-parser-binary-${{ matrix.os }}
path: bin/log-parser-*
outputs:
version: ${{ steps.build-info.outputs.version }}
#!/usr/bin/env -S runhaskell
{-# LANGUAGE CPP #-}
import Data.Char (toLower)
import Data.List (intercalate)
import Distribution.Package (packageVersion)
#if MIN_VERSION_Cabal(3, 8, 1)
import Distribution.Simple.PackageDescription (readGenericPackageDescription)
#else
import Distribution.PackageDescription.Parsec (readGenericPackageDescription)
#endif
import Distribution.System (buildArch, buildOS, buildPlatform)
import Distribution.Simple.BuildPaths (exeExtension)
import qualified Distribution.Verbosity as Verbosity
import Distribution.Version (versionNumbers)
main :: IO ()
main = do
packageDesc <- readGenericPackageDescription Verbosity.silent "log-parser.cabal"
let version = intercalate "." . map show . versionNumbers . packageVersion $ packageDesc
let os = map toLower . show $ buildOS
let arch = map toLower . show $ buildArch
let ext = exeExtension buildPlatform
setOutput "version" version
setOutput "os" os
setOutput "arch" arch
setOutput "ext" $ if null ext then ext else "." <> ext
-- | Set output for a GitHub action.
-- https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-output-parameter
setOutput :: String -> String -> IO ()
setOutput name value = putStrLn $ name <> "=" <> value
#!/usr/bin/env bash
set -euxo pipefail
HERE="$(builtin cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ ! -d "${HERE}/.venv" ]]; then
python3 -m venv "${HERE}/.venv"
"${HERE}/.venv/bin/pip" install requests
fi
exec "${HERE}/.venv/bin/python3" "${HERE}/make_release.py" "$@"
# pyright: strict, reportUnknownMemberType=false
from __future__ import annotations
import itertools
import json
import logging
import os
import requests
from pathlib import Path
from typing import Any
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
def main():
gh_token = os.environ["gh_token"]
version = os.environ["version"]
bindir = os.environ["bindir"]
sdistdir = os.environ["sdistdir"]
repo = os.environ["GITHUB_REPOSITORY"]
sha = os.environ["GITHUB_SHA"]
version_name = f"v{version}"
# check inputs
# ensure release files exist
gh_release_files = [
Path(bindir) / f"log-parser-{version}-linux-x86_64",
Path(bindir) / f"log-parser-{version}-osx-x86_64",
Path(bindir) / f"log-parser-{version}-windows-x86_64"
]
sdist_archive = Path(sdistdir) / f"log-parser-{version}.tar.gz"
all_files = gh_release_files + [sdist_archive]
for file in all_files:
if not file.exists():
raise Exception(f"File does not exist: {file}")
file_paths = [file.as_posix() for file in all_files]
logger.info(f"Creating release {version_name} with files: {file_paths}")
# check + parse CHANGELOG
changelog = Path("CHANGELOG.md").read_text()
if not changelog.startswith(f"## log-parser {version}"):
raise Exception("CHANGELOG doesn't look updated")
version_changes = get_version_changes(changelog)
create_github_release(
repo=repo,
token=gh_token,
sha=sha,
version_name=version_name,
version_changes=version_changes,
files=gh_release_files,
)
logger.info(f"Released log-parser {version_name}!")
def get_version_changes(changelog: str) -> str:
lines = changelog.split("\n")
# skip initial '## log-parser X.Y.Z' line
lines = lines[1:]
# take lines until the next '## log-parser X.Y.Z' line
lines = itertools.takewhile(lambda line: not line.startswith("## log-parser "), lines)
return "\n".join(lines)
def create_github_release(
*,
repo: str,
token: str,
sha: str,
version_name: str,
version_changes: str,
files: list[Path],
):
session = init_session()
session.headers["Accept"] = "application/vnd.github.v3+json"
session.headers["Authorization"] = f"token {token}"
session.headers["User-Agent"] = repo
payload = {
"tag_name": version_name,
"target_commitish": sha,
"name": version_name,
"body": version_changes,
}
logger.debug(f"Creating release with: {json.dumps(payload)}")
create_resp = session.post(
f"https://api.github.com/repos/{repo}/releases",
json=payload,
)
# upload_url is in the format: "https://...{?name,label}"
upload_url = create_resp.json()["upload_url"]
upload_url = upload_url.replace("{?name,label}", "")
for file in files:
logger.debug(f"Uploading asset: {file}")
with file.open("rb") as f:
session.post(
upload_url,
headers={"Content-Type": "application/octet-stream"},
params={"name": file.name},
data=f,
)
def init_session() -> requests.Session:
session = requests.Session()
def _check_status(r: requests.Response, *args: Any, **kwargs: Any):
r.raise_for_status()
# https://github.com/python/typeshed/issues/7776
session.hooks["response"].append( # pyright: ignore[reportFunctionMemberAccess]
_check_status,
)
return session
if __name__ == "__main__":
main()
name: Release
on:
workflow_run:
workflows: [CI]
types:
- completed
jobs:
ci:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
uses: ./.github/workflows/ci.yml
release:
runs-on: ubuntu-latest
needs:
- ci
steps:
- uses: actions/checkout@v2
with:
ref: main
#- uses: actions/download-artifact@v3
# with:
# name: log-parser-binary-ubuntu-latest
# path: ./bin/
#- uses: actions/download-artifact@v3
# with:
# name: log-parser-binary-macos-latest
# path: ./bin/
- uses: actions/download-artifact@v3
with:
name: log-parser-binary-windows-latest
path: ./bin/
- name: Make release
run: scripts/make-release.sh
env:
gh_token: ${{ secrets.GITHUB_TOKEN }}
version: ${{ needs.ci.outputs.version }}
bindir: ./bin/
sdistdir: ./sdist/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment