Cut the release from the changelog heading on a green main instead of a hand-pushed tag
This commit is contained in:
@@ -49,3 +49,21 @@ jobs:
|
|||||||
- name: Lowest supported Go
|
- name: Lowest supported Go
|
||||||
if: ${{ !cancelled() && steps.changes.outputs.code == 'true' }}
|
if: ${{ !cancelled() && steps.changes.outputs.code == 'true' }}
|
||||||
run: docker build --build-arg GO_VERSION=1.22.12 .
|
run: docker build --build-arg GO_VERSION=1.22.12 .
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Gitea release from CHANGELOG.md
|
||||||
|
needs: test
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- run: python3 release-tooling/publish_release.py
|
||||||
|
env:
|
||||||
|
GITEA_API_URL: ${{ github.api_url }}
|
||||||
|
GITEA_REPOSITORY: ${{ github.repository }}
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
SHA: ${{ github.sha }}
|
||||||
|
|||||||
@@ -559,7 +559,8 @@ with no breaking change. From `v2` the module path carries `/vN`, so fences ship
|
|||||||
batched into as few majors as possible.
|
batched into as few majors as possible.
|
||||||
|
|
||||||
[`testdata/shipped_shape.txt`](testdata/shipped_shape.txt) pins every path, each
|
[`testdata/shipped_shape.txt`](testdata/shipped_shape.txt) pins every path, each
|
||||||
category's format and each column's datatype and nullability; a pull request that
|
category's format and the categories it reads, and each column's datatype and
|
||||||
|
nullability; a pull request that
|
||||||
changes it or `data/` adds its `CHANGELOG.md` entry, which CI checks. A removed,
|
changes it or `data/` adds its `CHANGELOG.md` entry, which CI checks. A removed,
|
||||||
renamed or retyped line is a major.
|
renamed or retyped line is a major.
|
||||||
|
|
||||||
@@ -667,10 +668,11 @@ renamed or retyped line is a major.
|
|||||||
- **Raising the lowest supported Go is a major.** A consumer building on it breaks,
|
- **Raising the lowest supported Go is a major.** A consumer building on it breaks,
|
||||||
which is the one test every rule above applies; Go's convention of a minor is not
|
which is the one test every rule above applies; Go's convention of a minor is not
|
||||||
followed.
|
followed.
|
||||||
- **A release is a Gitea release built from the changelog.** The tag alone serves
|
- **The changelog heading is the one spelling of a release; CI cuts the tag.** A
|
||||||
`go get`, but prebuilt binaries need release assets, and the body being the tag's
|
tag pushed by hand is served by `go get` at once, so a tag whose commit lacks its
|
||||||
changelog section keeps one text; a tag with no heading fails the workflow
|
heading is burnt, not fixed. The heading on a gate-passed `main` commit is the
|
||||||
rather than publishing an empty release.
|
trigger instead: the tag can land only there, and the Gitea release the same job
|
||||||
|
publishes keeps one text as its body and is where prebuilt binaries will attach.
|
||||||
- **A `--data-path` override rebinds every reference to the category it
|
- **A `--data-path` override rebinds every reference to the category it
|
||||||
replaces.** References bind against the merged tree, so once shipped data uses
|
replaces.** References bind against the merged tree, so once shipped data uses
|
||||||
`{.person}`, a consumer's `sv_SE/person.json` is what every shipped reference
|
`{.person}`, a consumer's `sv_SE/person.json` is what every shipped reference
|
||||||
@@ -837,13 +839,10 @@ in its own commit:
|
|||||||
REPIN=1 docker compose run --rm --user "$(id -u):$(id -g)" test
|
REPIN=1 docker compose run --rm --user "$(id -u):$(id -g)" test
|
||||||
```
|
```
|
||||||
|
|
||||||
To release, head `CHANGELOG.md` with the version's section in place of `Unreleased`,
|
To release, head `CHANGELOG.md` with the version's section in place of `Unreleased`
|
||||||
merge, then tag `main`; the release workflow publishes the Gitea release with that
|
and merge: once `main` passes the gate, CI tags that commit `vX.Y.Z` and publishes
|
||||||
section as its body:
|
the Gitea release with the section as its body. A top heading of `[Unreleased]`
|
||||||
|
publishes nothing.
|
||||||
```sh
|
|
||||||
git tag -a v0.1.0 -m v0.1.0 && git push origin v0.1.0
|
|
||||||
```
|
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
@@ -867,7 +866,7 @@ value.go the value proof: what a typed column or calc operand holds, chec
|
|||||||
data.go data loading: fs.FS folders/files -> namespace tree, multi-source merge
|
data.go data loading: fs.FS folders/files -> namespace tree, multi-source merge
|
||||||
cmd/fejkdata/ the fejkdata CLI
|
cmd/fejkdata/ the fejkdata CLI
|
||||||
data/ shipped data (JSON), embedded at build: locale folders + a misc folder
|
data/ shipped data (JSON), embedded at build: locale folders + a misc folder
|
||||||
release-tooling/ the Gitea release a tag publishes
|
release-tooling/ the release CI publishes from the changelog heading
|
||||||
testdata/ the pinned shipped shape (see Versioning)
|
testdata/ the pinned shipped shape (see Versioning)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +1,52 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Create the Gitea release for a tag, its body the tag's CHANGELOG.md section.
|
"""Publish the Gitea release that CHANGELOG.md's top heading names, tagging SHA.
|
||||||
|
|
||||||
Env: GITEA_API_URL, GITEA_REPOSITORY (owner/repo), GITEA_TOKEN, TAG (vX.Y.Z).
|
A top heading of `[Unreleased]`, or a version already released, publishes nothing.
|
||||||
|
Env: GITEA_API_URL, GITEA_REPOSITORY (owner/repo), GITEA_TOKEN, SHA.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import urllib.error
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
|
HEADING = re.compile(r"^## \[([^\]]+)\].*?$\n(.*?)(?=^## \[|\Z)", re.M | re.S)
|
||||||
def section(changelog: str, version: str) -> str | None:
|
|
||||||
m = re.search(rf"^## \[{re.escape(version)}\].*?$\n(.*?)(?=^## \[|\Z)", changelog, re.M | re.S)
|
|
||||||
return m.group(1).strip() if m else None
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def request(path: str, data: dict | None = None):
|
||||||
tag = os.environ["TAG"]
|
|
||||||
if not re.fullmatch(r"v\d+\.\d+\.\d+", tag):
|
|
||||||
print(f"{tag}: not a release tag; a release is vX.Y.Z", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
with open("CHANGELOG.md", encoding="utf-8") as f:
|
|
||||||
body = section(f.read(), tag[1:])
|
|
||||||
if body is None:
|
|
||||||
print(f"CHANGELOG.md has no `## [{tag[1:]}]` heading; add the section, then tag", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
f"{os.environ['GITEA_API_URL']}/repos/{os.environ['GITEA_REPOSITORY']}/releases",
|
f"{os.environ['GITEA_API_URL']}/repos/{os.environ['GITEA_REPOSITORY']}/{path}",
|
||||||
data=json.dumps({"body": body, "name": tag, "tag_name": tag}).encode(),
|
data=json.dumps(data).encode() if data else None,
|
||||||
headers={"Authorization": f"token {os.environ['GITEA_TOKEN']}", "Content-Type": "application/json"},
|
headers={"Authorization": f"token {os.environ['GITEA_TOKEN']}", "Content-Type": "application/json"},
|
||||||
)
|
)
|
||||||
with urllib.request.urlopen(req) as resp:
|
with urllib.request.urlopen(req) as resp:
|
||||||
print(json.load(resp)["html_url"])
|
return json.load(resp)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
with open("CHANGELOG.md", encoding="utf-8") as f:
|
||||||
|
top = HEADING.search(f.read())
|
||||||
|
if top is None:
|
||||||
|
print("CHANGELOG.md has no `## [...]` heading", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
version, body = top.group(1), top.group(2).strip()
|
||||||
|
if version == "Unreleased":
|
||||||
|
print("top heading is Unreleased; nothing to publish")
|
||||||
|
return 0
|
||||||
|
if not re.fullmatch(r"\d+\.\d+\.\d+", version):
|
||||||
|
print(f"top heading `[{version}]` is neither Unreleased nor X.Y.Z", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
tag = f"v{version}"
|
||||||
|
try:
|
||||||
|
print(f"{tag} already published: {request(f'releases/tags/{tag}')['html_url']}")
|
||||||
|
return 0
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
if e.code != 404:
|
||||||
|
raise
|
||||||
|
release = request("releases", {"body": body, "name": tag, "tag_name": tag, "target_commitish": os.environ["SHA"]})
|
||||||
|
print(release["html_url"])
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user