Settle the compatibility contract, the changelog and the changelog-cut Gitea release #17
@@ -0,0 +1,23 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Gitea release from CHANGELOG.md
|
||||
runs-on: ubuntu-24.04
|
||||
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 }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
@@ -26,11 +26,15 @@ jobs:
|
||||
else
|
||||
base='${{ github.event.before }}'
|
||||
fi
|
||||
if git diff --quiet "$base"...HEAD -- '*.go' go.mod go.sum Dockerfile .dockerignore .github data README.md; then
|
||||
if git diff --quiet "$base"...HEAD -- '*.go' go.mod go.sum Dockerfile .dockerignore .github data testdata README.md; then
|
||||
echo "code=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "code=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
if ! git diff --quiet "$base"...HEAD -- data testdata/shipped_shape.txt && git diff --quiet "$base"...HEAD -- CHANGELOG.md; then
|
||||
echo "::error::the shipped data changed without a CHANGELOG.md entry (see the README's Versioning)"
|
||||
exit 1
|
||||
fi
|
||||
# `docker build` streams the context to the daemon. A compose bind-mount of
|
||||
# `.` mounts an empty host dir instead, because the job is itself a container.
|
||||
- name: Latest supported Go
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# Rules
|
||||
|
||||
- Go runs only through `docker compose run --rm <test|vet|fmt|build>`; the merge gate is `docker build .`.
|
||||
- Tests first, in their own commit; the implementation follows in the next. A re-pin of seeded output is its own commit.
|
||||
- Tests first, in their own commit; the implementation follows in the next. A re-pin of seeded output or of `testdata/shipped_shape.txt` is its own commit.
|
||||
- A change under `data/` or to the shape pin adds its `CHANGELOG.md` entry under `Unreleased` in the same PR; what is major is the README's Versioning table.
|
||||
- One-line commit messages: no ticket prefix, no repo name, no authorship trailers.
|
||||
- Hard tabs. No comment by default; delete a restatement, a rationale, history, or a file preamble.
|
||||
- One spelling per result: reject the other at `New`, and let the error name the spelling to use.
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). A release's
|
||||
`Breaking` section comes first and names each rejected spelling with its
|
||||
replacement, and each removed path, column or flag.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- First release: the CLI, the library and the shipped data set.
|
||||
@@ -529,6 +529,37 @@ then costs about what its output costs: an unweighted pick is O(1) whatever the
|
||||
list's length, a weighted one O(log n), and long formats, deep nesting and many
|
||||
tokens add cost in proportion to the output.
|
||||
|
||||
## Versioning
|
||||
|
||||
Semver tags on `main`, `v0.1.0` first; one version covers the shipped data, the
|
||||
library and the CLI, and [`CHANGELOG.md`](CHANGELOG.md) names what each release
|
||||
changed. A consumer's data, code and scripts keep working across a minor or a patch:
|
||||
a minor only adds, and a major is the only release that changes what exists.
|
||||
|
||||
| Surface | Major | Minor |
|
||||
|---------|-------|-------|
|
||||
| Shipped data | remove or rename a path; change a category's format; remove a value, or change a weight or a repeat; add a reference from one shipped category into another | a path, a locale, a value in a list |
|
||||
| Records | remove, rename, retype or add a column; let a column be null | a record, as a new category |
|
||||
| Data format | a fence: a spelling `New` rejects that it accepted; an option, since it reserves a field name | a builtin |
|
||||
| CLI | remove or rename a flag, or change its default; change what an exit code means; change the bytes a `--format` writes, the `--list` layout, or what an error names | a flag, a format |
|
||||
| Library | change or remove an exported name; raise the lowest supported Go | an exported name, an option |
|
||||
|
||||
Seeded output is a promise within one version: same seed, same version, same
|
||||
data, same output. Any release may shift a stream, since a value added to a list
|
||||
moves every draw after it, so pin fixtures per version. An error's wording may
|
||||
improve in a minor; the path, rejected spelling and replacement it names may not.
|
||||
|
||||
Before `v1.0.0` a minor is the breaking unit: `0.(x+1).0` may carry a major's
|
||||
changes, each named in the changelog, and a `0.x.y` patch may not. `v1.0.0` is
|
||||
cut once the shipped data is in its record shape and one full minor has shipped
|
||||
with no breaking change. From `v2` the module path carries `/vN`, so fences ship
|
||||
batched into as few majors as possible.
|
||||
|
||||
[`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
|
||||
changes it or `data/` adds its `CHANGELOG.md` entry, which CI checks. A removed,
|
||||
renamed or retyped line is a major.
|
||||
|
||||
## Goals
|
||||
|
||||
1. **Valid by construction** — every value passes the check its real consumer
|
||||
@@ -613,14 +644,30 @@ tokens add cost in proportion to the output.
|
||||
folder, `..` the folder above — what those spellings already mean to anyone who
|
||||
has typed a path. A locale's files reach each other without naming the locale,
|
||||
so a folder renames and copies without editing its references.
|
||||
- **After the first tag, a new fence is a major version.** Data files are the
|
||||
public API, and one spelling per result grows by tightening, so every fence
|
||||
invalidates some file. Each such release names the rejected spelling and its
|
||||
replacement in the changelog and in the load error, and that is the whole
|
||||
migration: a fence rejects one spelling with one replacement, so the fix is
|
||||
local to each site. A fence that would need a non-local rewrite ships a
|
||||
converter with its release instead. Before the first tag there is no
|
||||
compatibility promise.
|
||||
- **A change to what exists is a major; a minor only adds.** Data files, the CLI
|
||||
and the Go API are the public API, and a consumer must be able to take a minor
|
||||
without an edit — so an added column is a major, since it changes the CSV header
|
||||
and the `INSERT` column list, as is a removed value, which changes what a fixture
|
||||
holds, and a new option, which reserves a field name. One spelling per result
|
||||
grows by tightening, so every fence invalidates some file. Each such release
|
||||
names the rejected spelling and its replacement in the changelog and in the load
|
||||
error, and that is the whole migration: a fence rejects one spelling with one
|
||||
replacement, so the fix is local to each site. A fence that would need a
|
||||
non-local rewrite ships a converter with its release instead. Before `v1.0.0` a
|
||||
minor carries what a major would.
|
||||
- **Seeded output is promised within one version.** Any edit to a category shifts
|
||||
its stream and everything drawn after it, so a promise across versions would
|
||||
freeze every shipped list; a fixture is re-pinned on a bump, as this repo's own are.
|
||||
- **An error is a contract by what it names, not its bytes.** A script branches on
|
||||
the exit code and reads the named path or spelling, so those hold; wording improves
|
||||
in a minor.
|
||||
- **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
|
||||
followed.
|
||||
- **A release is a Gitea release built from the changelog.** The tag alone serves
|
||||
`go get`, but prebuilt binaries need release assets, and the body being the tag's
|
||||
changelog section keeps one text; a tag with no heading fails the workflow
|
||||
rather than publishing an empty release.
|
||||
- **A `--data-path` override rebinds every reference to the category it
|
||||
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
|
||||
@@ -779,6 +826,21 @@ docker build --build-arg GO_VERSION=1.22.12 . # lowest supported
|
||||
GO_VERSION=1.22.12 docker compose run --rm test # the same tests, without the image build
|
||||
```
|
||||
|
||||
A change to the shipped data re-pins [`testdata/shipped_shape.txt`](testdata/shipped_shape.txt)
|
||||
in its own commit:
|
||||
|
||||
```sh
|
||||
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`,
|
||||
merge, then tag `main`; the release workflow publishes the Gitea release with that
|
||||
section as its body:
|
||||
|
||||
```sh
|
||||
git tag -a v0.1.0 -m v0.1.0 && git push origin v0.1.0
|
||||
```
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
@@ -801,6 +863,8 @@ 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
|
||||
cmd/fejkdata/ the fejkdata CLI
|
||||
data/ shipped data (JSON), embedded at build: locale folders + a misc folder
|
||||
release-tooling/ the Gitea release a tag publishes
|
||||
testdata/ the pinned shipped shape (see Versioning)
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
@@ -24,6 +24,7 @@ x-go: &go
|
||||
HOME: /cache
|
||||
GOCACHE: /cache/build
|
||||
GOMODCACHE: /cache/mod
|
||||
REPIN: ${REPIN:-}
|
||||
|
||||
services:
|
||||
test:
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create the Gitea release for a tag, its body the tag's CHANGELOG.md section.
|
||||
|
||||
Env: GITEA_API_URL, GITEA_REPOSITORY (owner/repo), GITEA_TOKEN, TAG (vX.Y.Z).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
|
||||
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:
|
||||
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(
|
||||
f"{os.environ['GITEA_API_URL']}/repos/{os.environ['GITEA_REPOSITORY']}/releases",
|
||||
data=json.dumps({"body": body, "name": tag, "tag_name": tag}).encode(),
|
||||
headers={"Authorization": f"token {os.environ['GITEA_TOKEN']}", "Content-Type": "application/json"},
|
||||
)
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
print(json.load(resp)["html_url"])
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -7,24 +7,15 @@
|
||||
- Major data update. Shipped categories render as records with their building
|
||||
blocks as columns (`sv_SE.person` → `femalefirst`, `malefirst`; `misc.uuid` →
|
||||
`variant`), and `sv_SE.address` draws its postal code apart from its locality.
|
||||
It also settles what a version promises about shipped data: its paths, its
|
||||
record columns with their datatypes and nulls — a column of one reference alone
|
||||
takes both, so typing a column or adding a null breaks its readers — and whether
|
||||
a seed renders the same output across versions.
|
||||
- `email.local` and `username` share their handle lists, while their name
|
||||
variants differ on purpose. Share the lists only if that is a clean win.
|
||||
variants differ on purpose. Share the lists only if that is a clean win — a
|
||||
reference between shipped categories is a major once tagged.
|
||||
|
||||
### Release
|
||||
|
||||
- Versioning — semver tags, starting at `v0.1.0`; `v1.0.0` once the grammar
|
||||
settles. From v2 the module path carries `/vN` (`go.mod`, imports, the README's
|
||||
install lines), so fences ship batched into as few majors as possible. Reword
|
||||
the Decision "After the first tag, a new fence is a major version" to match:
|
||||
before `v1.0.0` a fence ships in a minor.
|
||||
- Changelog — `CHANGELOG.md`, started with `v0.1.0`; the fence Decision's
|
||||
"changelog" links there.
|
||||
- CLI without Go — investigate prebuilt binaries: GoReleaser publishing to Gitea
|
||||
releases, a container image, Homebrew and Scoop.
|
||||
- CLI without Go — investigate prebuilt binaries: GoReleaser attaching them to
|
||||
the Gitea release the tag workflow publishes, a container image, Homebrew and
|
||||
Scoop. A `--version` flag lands with them.
|
||||
- Homepage — a simple page for fejkdata with an in-browser generator: the library
|
||||
compiled to WebAssembly, so visitors generate as much data as they like in their
|
||||
own browser.
|
||||
|
||||
Reference in New Issue
Block a user