Settle the compatibility contract, the changelog and the tag-published Gitea release
Tests / vet + fmt + tests (pull_request) Successful in 1m7s
Tests / vet + fmt + tests (pull_request) Successful in 1m7s
This commit is contained in:
@@ -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
|
else
|
||||||
base='${{ github.event.before }}'
|
base='${{ github.event.before }}'
|
||||||
fi
|
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"
|
echo "code=false" >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
echo "code=true" >> "$GITHUB_OUTPUT"
|
echo "code=true" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
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
|
# `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.
|
# `.` mounts an empty host dir instead, because the job is itself a container.
|
||||||
- name: Latest supported Go
|
- name: Latest supported Go
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# Rules
|
# Rules
|
||||||
|
|
||||||
- Go runs only through `docker compose run --rm <test|vet|fmt|build>`; the merge gate is `docker build .`.
|
- 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.
|
- 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.
|
- 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.
|
- 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
|
list's length, a weighted one O(log n), and long formats, deep nesting and many
|
||||||
tokens add cost in proportion to the output.
|
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
|
## Goals
|
||||||
|
|
||||||
1. **Valid by construction** — every value passes the check its real consumer
|
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
|
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,
|
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.
|
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
|
- **A change to what exists is a major; a minor only adds.** Data files, the CLI
|
||||||
public API, and one spelling per result grows by tightening, so every fence
|
and the Go API are the public API, and a consumer must be able to take a minor
|
||||||
invalidates some file. Each such release names the rejected spelling and its
|
without an edit — so an added column is a major, since it changes the CSV header
|
||||||
replacement in the changelog and in the load error, and that is the whole
|
and the `INSERT` column list, as is a removed value, which changes what a fixture
|
||||||
migration: a fence rejects one spelling with one replacement, so the fix is
|
holds, and a new option, which reserves a field name. One spelling per result
|
||||||
local to each site. A fence that would need a non-local rewrite ships a
|
grows by tightening, so every fence invalidates some file. Each such release
|
||||||
converter with its release instead. Before the first tag there is no
|
names the rejected spelling and its replacement in the changelog and in the load
|
||||||
compatibility promise.
|
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
|
- **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
|
||||||
@@ -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
|
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
|
## 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
|
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
|
||||||
|
testdata/ the pinned shipped shape (see Versioning)
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ x-go: &go
|
|||||||
HOME: /cache
|
HOME: /cache
|
||||||
GOCACHE: /cache/build
|
GOCACHE: /cache/build
|
||||||
GOMODCACHE: /cache/mod
|
GOMODCACHE: /cache/mod
|
||||||
|
REPIN: ${REPIN:-}
|
||||||
|
|
||||||
services:
|
services:
|
||||||
test:
|
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
|
- Major data update. Shipped categories render as records with their building
|
||||||
blocks as columns (`sv_SE.person` → `femalefirst`, `malefirst`; `misc.uuid` →
|
blocks as columns (`sv_SE.person` → `femalefirst`, `malefirst`; `misc.uuid` →
|
||||||
`variant`), and `sv_SE.address` draws its postal code apart from its locality.
|
`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
|
- `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
|
### Release
|
||||||
|
|
||||||
- Versioning — semver tags, starting at `v0.1.0`; `v1.0.0` once the grammar
|
- CLI without Go — investigate prebuilt binaries: GoReleaser attaching them to
|
||||||
settles. From v2 the module path carries `/vN` (`go.mod`, imports, the README's
|
the Gitea release the tag workflow publishes, a container image, Homebrew and
|
||||||
install lines), so fences ship batched into as few majors as possible. Reword
|
Scoop. A `--version` flag lands with them.
|
||||||
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.
|
|
||||||
- Homepage — a simple page for fejkdata with an in-browser generator: the library
|
- 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
|
compiled to WebAssembly, so visitors generate as much data as they like in their
|
||||||
own browser.
|
own browser.
|
||||||
|
|||||||
Reference in New Issue
Block a user