diff --git a/.github/check-version.sh b/.github/check-version.sh new file mode 100755 index 0000000..ca236e3 --- /dev/null +++ b/.github/check-version.sh @@ -0,0 +1,85 @@ +#!/bin/sh +# +# Copyright (c) OpenIPC https://openipc.org MIT License +# +# check-version.sh — fail a pull request that changes the code or the build +# but not VTESSE_VERSION in include/vtesse/version.h. A new version must be +# MAJOR.MINOR.PATCH without leading zeros, greater than the base one, and +# have its section in CHANGELOG.md ("## — YYYY-MM-DD") that the +# base branch does not have yet. The sources, android/ (except its unit +# tests), the Makefile, ci/ and scripts/ count as code or build, without the +# Markdown files in them (server/README.md, android/README.md); docs, +# tests/, bench/, screenshots and the workflows in .github/ need no new +# version. The format is checked only when the version changes, so the +# MAJOR.MM versions up to 1.12 stay valid until the next code change. +# +# Usage: .github/check-version.sh [BASE] (default origin/main; CI passes +# HEAD^1, the base side of the pull request's merge commit) +# + +set -eu + +base=${1:-origin/main} +header=include/vtesse/version.h +code="include netcore cli vtun port server scripts third_party tools android Makefile ci" + +# The one VTESSE_VERSION definition (comments and other text do not count). +ver() { + v=$(sed -n 's/^#define VTESSE_VERSION "\([^"]*\)"[[:space:]]*$/\1/p') + if [ "$(printf '%s\n' "$v" | grep -c .)" -ne 1 ]; then + echo "::error::$1:$header: expected one VTESSE_VERSION definition," \ + "found: $(printf '%s' "$v" | tr '\n' ' ')" >&2 + exit 1 + fi + printf '%s\n' "$v" +} + +new=$(ver HEAD < "$header") +old=$(git show "$base:$header" | ver "$base") + +# shellcheck disable=SC2086 # $code is a list of paths +changed=$(git diff --name-only "$base"...HEAD -- $code ':(exclude)android/app/src/test' \ + ':(exclude,glob)**/*.md') + +if [ "$new" = "$old" ]; then + if [ -n "$changed" ]; then + echo "::error::code or build changed, VTESSE_VERSION is still $old:" \ + "bump PATCH (fix) or MINOR (feature) and add its CHANGELOG.md section" + echo "$changed" + exit 1 + fi + echo "version $new unchanged, no code or build changes" + exit 0 +fi + +if ! echo "$new" | grep -Eq '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$'; then + echo "::error::VTESSE_VERSION \"$new\" is not MAJOR.MINOR.PATCH" + exit 1 +fi + +# A MAJOR.MM version from before MAJOR.MINOR.PATCH is MAJOR.MM.0: 1.12.0 is 1.12. +case $old in +*.*.*) oldn=$old ;; +*) oldn=$old.0 ;; +esac +top=$(printf '%s\n%s\n' "$oldn" "$new" | sort -V | tail -n 1) +if [ "$new" = "$oldn" ] || [ "$top" != "$new" ]; then + echo "::error::VTESSE_VERSION $new is not greater than $old" + exit 1 +fi + +# $new is digits and dots only (checked above): escape the dots, require a date. +re=$(printf '%s' "$new" | sed 's/\./\\./g') +if ! grep -Eq "^## $re — [0-9]{4}-[0-9]{2}-[0-9]{2}\$" CHANGELOG.md; then + echo "::error::CHANGELOG.md has no section \"## $new — YYYY-MM-DD\"" + exit 1 +fi + +# The section must come with this pull request, not be one the base +# branch already has (a version taken from there brings no notes). +if git show "$base:CHANGELOG.md" 2>/dev/null | grep -Eq "^## $re — "; then + echo "::error::the section \"## $new\" is already on $base: take a new version" + exit 1 +fi + +echo "version $old -> $new, CHANGELOG.md section found" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4a49406..207ce03 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,9 +14,11 @@ # (ci/build-musl.sh). # # Release: one release only, "latest", replaced by every push to main. Its title -# carries the version from include/vtesse/version.h (VTESSE_VERSION "MAJOR.MM") -# and its notes are one short line; the change history lives in CHANGELOG.md. -# No versioned releases are kept. +# carries the version from include/vtesse/version.h (VTESSE_VERSION, +# MAJOR.MINOR.PATCH) and its notes are one short line; the change history lives +# in CHANGELOG.md. No versioned releases are kept. +# On pull requests, .github/check-version.sh checks that a change to the code or +# the build comes with a new VTESSE_VERSION and its CHANGELOG.md section. # name: VTesse CI @@ -28,13 +30,32 @@ on: branches: [main] workflow_dispatch: -# One run per branch and event: a newer push cancels the older run, so -# "latest" always carries the build of the newest commit. +# One run per branch: on pull requests a newer push cancels the older run; on +# main it waits instead (only the newest waits), so "latest" still ends with the +# build of the newest commit and the release job is never stopped between +# deleting the old "latest" and creating the new one. A manual run shares the +# push group. concurrency: - group: build-${{ github.event_name }}-${{ github.ref }} - cancel-in-progress: true + group: build-${{ github.event_name == 'pull_request' && 'pull_request' || 'push' }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: + # ---------------------------------------------------------------- version + version: + name: Version bump + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + # The checkout is the pull request's merge commit: its first parent is the + # base it was made from, so both versions come from the same base. + - name: Code changes come with a new version + run: .github/check-version.sh HEAD^1 + # ------------------------------------------------------------------ tests tests: name: Unit tests (native, sanitizers) @@ -185,7 +206,9 @@ jobs: release: name: Release needs: [tests, build, bench, android] - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + # a manual run of main publishes too: it shares the push group and may take the + # place of a waiting push, so it must not skip the release + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest permissions: contents: write diff --git a/CHANGELOG.md b/CHANGELOG.md index 649b0b7..8d2247e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,9 @@ The version lives in `include/vtesse/version.h`. Binaries are published only as the [`latest`](https://github.com/OpenIPC/vtesse/releases/tag/latest) release, rebuilt -from every push to `main`; this file is the change history. +from every push to `main`; this file is the change history. Versions after 1.12 are +`MAJOR.MINOR.PATCH` (1.12 is followed by 1.12.1 or 1.13.0); the headings below keep the +numbers as released. ## 1.12 — 2026-10-15 diff --git a/docs/testing.md b/docs/testing.md index 7906c6c..d9bd89e 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -32,16 +32,23 @@ and pull request: ### Versions and releases The version lives in one place, `include/vtesse/version.h`: -`#define VTESSE_VERSION "MAJOR.MM"` (two-digit minor: `1.00`, `1.01`, …; shown by -`vtesse-cli --version`). +`#define VTESSE_VERSION "MAJOR.MINOR.PATCH"` (shown by `vtesse-cli --version`). Up to 1.12 +it was `MAJOR.MM` (`1.00`, `1.01`, …); 1.12 is followed by 1.12.1 or 1.13.0. - There is one release, `latest`: every push to `main` replaces it (tag moved to that commit). No versioned releases are kept. -- Its title is `VTesse MAJOR.MM`; the notes are one short line. The change history is +- Its title is `VTesse `; the notes are one short line. The change history is [CHANGELOG.md](../CHANGELOG.md), the patch list [server/README.md](../server/README.md). - File names are stable: `vtesse-`, `vtesse-tap-`, and `vtund--x86_64` (e.g. `vtund-3.0.2-x86_64`) for the VTun server, PCs only. -- A new version: bump `VTESSE_VERSION` and add a `CHANGELOG.md` entry in the same commit. +- PATCH for fixes and small changes, MINOR for a new feature, MAJOR for an incompatible + change or a large update, only when the maintainer decides. A pull request that changes + the code or the build (the sources, `android/` except its unit tests, the `Makefile`, + `ci/`, `scripts/`) bumps `VTESSE_VERSION` as its last commit before the merge, together + with its `CHANGELOG.md` section (`## — YYYY-MM-DD`). Docs, `tests/`, `bench/` + and `.github/` changes do not. CI checks this on every pull request + (`.github/check-version.sh`): a new number must be canonical (no leading zeros), greater + than the base one, and come with its own section. ## Test bench