Skip to content

Commit b618917

Browse files
Add release workflow
1 parent d4057d4 commit b618917

File tree

7 files changed

+226
-7
lines changed

7 files changed

+226
-7
lines changed

.github/workflows/ci.yml

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ on:
44
push:
55
branches:
66
- main
7+
workflow_call:
78

89
jobs:
910
build_and_test:

.github/workflows/release.yml

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Release
2+
on: workflow_dispatch
3+
4+
jobs:
5+
ci:
6+
uses: ./.github/workflows/ci.yml
7+
8+
release:
9+
runs-on: ubuntu-latest
10+
needs:
11+
- ci
12+
13+
steps:
14+
- uses: actions/checkout@v3
15+
with:
16+
ref: main
17+
18+
- uses: actions/download-artifact@v3
19+
with:
20+
name: persistent-mtl-sdist
21+
path: ./sdist/
22+
23+
- name: Load package version
24+
run: scripts/GetVersion.hs >> "${GITHUB_OUTPUT}"
25+
id: version_info
26+
27+
- name: Load Hackage token secret name
28+
run: |
29+
python >> "${GITHUB_OUTPUT}" <<EOF
30+
import re
31+
username = "${{ github.actor }}"
32+
secret_name = "HACKAGE_TOKEN_" + re.sub(r"\W+", "_", username).upper()
33+
print(f"secret_name={secret_name}")
34+
EOF
35+
id: hackage_token_secret
36+
37+
- name: Make release
38+
run: scripts/make-release.sh
39+
env:
40+
gh_token: ${{ secrets.GITHUB_TOKEN }}
41+
hackage_token: ${{ secrets[steps.hackage_token_secret.outputs.secret_name] }}
42+
version: ${{ steps.version_info.outputs.version }}
43+
sdistdir: ./sdist/

.pre-commit-config.yaml

+15
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,18 @@ repos:
2929
language: system
3030
entry: fourmolu -i
3131
files: '\.hs$'
32+
33+
- repo: https://github.com/psf/black
34+
rev: '22.10.0'
35+
hooks:
36+
- id: black
37+
38+
- repo: local
39+
hooks:
40+
- id: pyright
41+
name: pyright
42+
entry: pyright
43+
language: node
44+
pass_filenames: false
45+
types: [python]
46+
additional_dependencies: ['[email protected]']

CHANGELOG.md

+7-7
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,37 @@
1111
* Add `catchSqlTransaction`
1212
* Add `retryCallback` to `SqlQueryEnv`
1313

14-
# 0.4.0.0
14+
# v0.4.0.0
1515

1616
* Add some mtl instances: `MonadThrow`, `MonadCatch`, `MonadMask`, `MonadLogger`, `MonadReader`
1717
* Removed support for GHC 8.2, 8.4
1818
* Add `MonadSqlQuery (TransactionM m)` superclass constraint to allow writing functions generic on some `MonadSqlQuery m` using `withTransaction`, as shown in examples in README
1919

20-
# 0.3.0.0
20+
# v0.3.0.0
2121

2222
* Add `unsafeLiftSql` ([#38](https://github.com/brandonchinn178/persistent-mtl/pull/38))
2323

24-
# 0.2.2.0
24+
# v0.2.2.0
2525

2626
* Fix for persistent 2.13
2727

28-
# 0.2.1.0
28+
# v0.2.1.0
2929

3030
* Add `rerunnableLift` for `SqlTransaction`
3131
* Use `unliftio-pool` instead of `resourcet-pool`, which has better async exeception safety
3232

33-
# 0.2.0.0
33+
# v0.2.0.0
3434

3535
* Use a separate monad within `withTransaction` to prevent unsafe/arbitrary IO actions ([#7](https://github.com/brandonchinn178/persistent-mtl/issues/7), [#28](https://github.com/brandonchinn178/persistent-mtl/issues/28))
3636
* Add `MonadRerunnableIO` to support IO actions within `withTransaction` only if the IO action is determined to be rerunnable
3737
* Add built-in support for retrying transactions if a serialization error occurs
3838
* Remove `SqlQueryRep` as an export from `Database.Persist.Monad`. You shouldn't ever need it for normal usage. It is now re-exported by `Database.Persist.Monad.TestUtils`, since most of the usage of `SqlQueryRep` is in mocking queries. If you need it otherwise, you can import it directly from `Database.Persist.Monad.SqlQueryRep`.
3939

40-
# 0.1.0.1
40+
# v0.1.0.1
4141

4242
Fix quickstart
4343

44-
# 0.1.0.0
44+
# v0.1.0.0
4545

4646
Initial release
4747
* `SqlQueryT` + `MonadSqlQuery`

scripts/GetVersion.hs

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/usr/bin/env stack
2+
{- stack runghc --package Cabal -}
3+
4+
import Data.List (intercalate)
5+
import Distribution.Package (packageVersion)
6+
import Distribution.PackageDescription.Parsec (readGenericPackageDescription)
7+
import qualified Distribution.Verbosity as Verbosity
8+
import Distribution.Version (versionNumbers)
9+
10+
main :: IO ()
11+
main = do
12+
packageDesc <- readGenericPackageDescription Verbosity.silent "persistent-mtl.cabal"
13+
let version = intercalate "." . map show . versionNumbers . packageVersion $ packageDesc
14+
-- https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-output-parameter
15+
putStrLn $ "version=" ++ version

scripts/make-release.sh

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/usr/bin/env bash
2+
3+
set -euxo pipefail
4+
HERE="$(builtin cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
6+
if [[ ! -d "${HERE}/.venv" ]]; then
7+
python3 -m venv "${HERE}/.venv"
8+
"${HERE}/.venv/bin/pip" install requests
9+
fi
10+
11+
exec "${HERE}/.venv/bin/python3" "${HERE}/make_release.py" "$@"

scripts/make_release.py

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# pyright: strict, reportUnknownMemberType=false
2+
3+
from __future__ import annotations
4+
5+
import itertools
6+
import json
7+
import logging
8+
import os
9+
import re
10+
import requests
11+
from pathlib import Path
12+
from typing import Any
13+
14+
logger = logging.getLogger(__name__)
15+
logging.basicConfig(level=logging.DEBUG)
16+
17+
18+
def main():
19+
gh_token = os.environ["gh_token"]
20+
hackage_token = os.environ["hackage_token"]
21+
version = os.environ["version"]
22+
sdistdir = os.environ["sdistdir"]
23+
repo = os.environ["GITHUB_REPOSITORY"]
24+
sha = os.environ["GITHUB_SHA"]
25+
26+
version_name = f"v{version}"
27+
28+
# check inputs
29+
if not hackage_token:
30+
raise Exception(
31+
"Hackage token is not provided (did you add a Secret of the form HACKAGE_TOKEN_<github username>?)"
32+
)
33+
34+
# ensure sdist exists
35+
sdist_archive = Path(sdistdir) / f"persistent-mtl-{version}.tar.gz"
36+
if not sdist_archive.exists():
37+
raise Exception(f"File does not exist: {sdist_archive}")
38+
39+
# check + parse CHANGELOG
40+
changelog = Path("CHANGELOG.md").read_text()
41+
changelog = re.sub(r"^# Unreleased\n+", "", changelog)
42+
if not changelog.startswith(f"# {version_name}"):
43+
raise Exception("CHANGELOG doesn't look updated")
44+
version_changes = get_version_changes(changelog)
45+
46+
logger.info(f"Creating release {version_name}")
47+
create_github_release(
48+
repo=repo,
49+
token=gh_token,
50+
sha=sha,
51+
version_name=version_name,
52+
version_changes=version_changes,
53+
)
54+
55+
# uploading as candidate because uploads are irreversible, unlike
56+
# GitHub releases, so just to be extra sure, we'll upload this as
57+
# a candidate and manually confirm uploading the package on Hackage
58+
upload_hackage_candidate(
59+
token=hackage_token,
60+
archive=sdist_archive,
61+
)
62+
63+
logger.info(f"Released persistent-mtl {version_name}!")
64+
65+
66+
def get_version_changes(changelog: str) -> str:
67+
lines = changelog.split("\n")
68+
69+
# skip initial '# vX.Y.Z' line
70+
lines = lines[1:]
71+
72+
# take lines until the next '# vX.Y.Z' line
73+
lines = itertools.takewhile(lambda line: not line.startswith("# v"), lines)
74+
75+
return "\n".join(lines)
76+
77+
78+
def create_github_release(
79+
*,
80+
repo: str,
81+
token: str,
82+
sha: str,
83+
version_name: str,
84+
version_changes: str,
85+
):
86+
session = init_session()
87+
session.headers["Accept"] = "application/vnd.github.v3+json"
88+
session.headers["Authorization"] = f"token {token}"
89+
session.headers["User-Agent"] = repo
90+
91+
payload = {
92+
"tag_name": version_name,
93+
"target_commitish": sha,
94+
"name": version_name,
95+
"body": version_changes,
96+
}
97+
logger.debug(f"Creating release with: {json.dumps(payload)}")
98+
99+
session.post(
100+
f"https://api.github.com/repos/{repo}/releases",
101+
json=payload,
102+
)
103+
104+
105+
def upload_hackage_candidate(
106+
*,
107+
token: str,
108+
archive: Path,
109+
):
110+
session = init_session()
111+
with archive.open("rb") as f:
112+
session.post(
113+
"https://hackage.haskell.org/packages/candidates",
114+
headers={"Authorization": f"X-ApiKey {token}"},
115+
files={"package": f},
116+
)
117+
118+
119+
def init_session() -> requests.Session:
120+
session = requests.Session()
121+
122+
def _check_status(r: requests.Response, *args: Any, **kwargs: Any):
123+
r.raise_for_status()
124+
125+
# https://github.com/python/typeshed/issues/7776
126+
session.hooks["response"].append( # pyright: ignore[reportFunctionMemberAccess]
127+
_check_status,
128+
)
129+
130+
return session
131+
132+
133+
if __name__ == "__main__":
134+
main()

0 commit comments

Comments
 (0)