Add multiplatform CI governance gates

This commit is contained in:
m1ngsama 2026-05-28 12:05:55 +08:00
parent d4b260c160
commit b6f92968d0
18 changed files with 775 additions and 184 deletions

View file

@ -2,22 +2,34 @@ name: CI
on:
push:
branches: [ main ]
branches: [ main, 'release/**' ]
pull_request:
branches: [ main ]
branches: [ main, 'release/**' ]
workflow_dispatch:
schedule:
- cron: '17 3 * * *'
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-and-test:
pr-gate:
name: PR gate (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
os: [ubuntu-24.04, macos-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install dependencies (Ubuntu)
if: runner.os == 'Linux'
@ -42,11 +54,26 @@ jobs:
- name: Run release preflight
run: make release-check
extended-linux-runtime:
name: Extended Linux runtime
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y expect libssh-dev valgrind
- name: Run extended release preflight
run: |
RUN_INTEGRATION=1 RUN_SOAK=1 RUN_SLOW_CLIENT=1 make release-check
- name: Check for memory leaks
if: runner.os == 'Linux'
run: |
set -eu
sudo apt-get install -y valgrind
STATE_DIR=$(mktemp -d)
SERVER_LOG="$STATE_DIR/server.log"
VALGRIND_LOG="$STATE_DIR/valgrind.log"
@ -101,3 +128,60 @@ jobs:
cat "$VALGRIND_LOG"
exit 1
fi
portable-container-builds:
name: Portable build (${{ matrix.name }})
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
include:
- name: debian-stable-glibc
image: debian:stable-slim
setup: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends build-essential libssh-dev ca-certificates
- name: ubuntu-24.04-glibc
image: ubuntu:24.04
setup: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends build-essential libssh-dev ca-certificates
- name: alpine-musl
image: alpine:3.20
setup: apk add --no-cache build-base libssh-dev ca-certificates
steps:
- uses: actions/checkout@v6
- name: Build in container
run: |
docker run --rm -v "$PWD:/src:ro" "${{ matrix.image }}" sh -c '
set -eu
${{ matrix.setup }}
mkdir /work
cp -R /src/. /work/
cd /work
make clean
make
./tnt --version
./tntctl --version
'
package-recipe-gate:
name: Package recipe gate
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Install packaging tools
run: |
sudo apt-get update
sudo apt-get install -y ruby cpio
- name: Validate packaging metadata
run: |
for script in scripts/*.sh; do
sh -n "$script"
done
bash -n packaging/arch/PKGBUILD
ruby -c packaging/homebrew/tnt-chat.rb
scripts/package_debian_source.sh "$RUNNER_TEMP/debian-source"

View file

@ -8,11 +8,15 @@ on:
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
build:
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04
@ -33,7 +37,7 @@ jobs:
ctl_artifact: tntctl-darwin-arm64
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Verify release tag matches source version
run: scripts/check_release_ref.sh "${GITHUB_REF_NAME}"
@ -91,40 +95,91 @@ jobs:
${{ matrix.artifact }}
${{ matrix.ctl_artifact }}
source-archive:
name: Source archive
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Verify release tag matches source version
run: scripts/check_release_ref.sh "${GITHUB_REF_NAME}"
- name: Build source archive
run: |
set -eu
version=$(sed -n 's/^#define TNT_VERSION "\([^"]*\)".*/\1/p' include/common.h)
commit=$(git rev-list -n 1 "${GITHUB_REF_NAME}")
mkdir -p dist
git archive --format=tar.gz --prefix="TNT-${version}/" \
-o "dist/tnt-chat-v${version}-source.tar.gz" "$commit"
tar -tzf "dist/tnt-chat-v${version}-source.tar.gz" >/dev/null
tar -tzf "dist/tnt-chat-v${version}-source.tar.gz" | grep -q "^TNT-${version}/LICENSE$"
tar -tzf "dist/tnt-chat-v${version}-source.tar.gz" | grep -q "^TNT-${version}/packaging/README.md$"
sha256sum "dist/tnt-chat-v${version}-source.tar.gz"
- name: Upload source archive
uses: actions/upload-artifact@v4
with:
name: tnt-chat-source
path: dist/tnt-chat-v*-source.tar.gz
artifact-gate:
name: Release artifact gate
needs: [build, source-archive]
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Verify release tag matches source version
run: scripts/check_release_ref.sh "${GITHUB_REF_NAME}"
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
path: ./artifacts
- name: Verify and package release assets
run: scripts/package_release_assets.sh ./artifacts ./dist/release-assets
- name: Upload release asset bundle
uses: actions/upload-artifact@v4
with:
name: release-assets
path: dist/release-assets/*
release:
needs: build
needs: [artifact-gate, source-archive]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Verify release tag matches source version
run: scripts/check_release_ref.sh "${GITHUB_REF_NAME}"
- name: Download all artifacts
- name: Download release asset bundle
uses: actions/download-artifact@v4
with:
path: ./artifacts
name: release-assets
path: ./release-assets
- name: Create checksums
- name: Verify release checksums
run: |
cd artifacts
: > checksums.txt
for artifact in */tnt-* */tntctl-*; do
[ -f "$artifact" ] || continue
sha256sum "$artifact" | sed "s# $artifact# $(basename "$artifact")#" >> checksums.txt
done
cd release-assets
sha256sum -c checksums.txt
cat checksums.txt
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: |
artifacts/*/tnt-*
artifacts/*/tntctl-*
artifacts/checksums.txt
release-assets/*
body: |
## Installation
@ -190,11 +245,16 @@ jobs:
sha256sum -c checksums.txt --ignore-missing
# macOS
for f in tnt-* tntctl-*; do
for f in tnt-* tntctl-* tnt-chat-*-source.tar.gz; do
grep " $f$" checksums.txt | shasum -a 256 -c -
done
```
The release also includes `tnt-chat-${{ github.ref_name }}-source.tar.gz`
for package-manager recipes. Verify it with the same `checksums.txt`
before updating Arch, Homebrew, Debian, Ubuntu, or container package
metadata.
## What's Changed
See [docs/CHANGELOG.md](https://github.com/${{ github.repository }}/blob/${{ github.ref_name }}/docs/CHANGELOG.md)
draft: true

View file

@ -134,6 +134,7 @@ script-test: all
@cd tests && ./test_docs_help_surface.sh
@cd tests && ./test_logrotate.sh
@cd tests && ./test_message_log_tool.sh
@cd tests && ./test_release_artifact_gate.sh
integration-test: all
@echo "Running integration tests..."

View file

@ -398,14 +398,14 @@ make release-check
Longer local preflight can opt into runtime soak and slow-client coverage:
```sh
RUN_SOAK=1 RUN_SLOW_CLIENT=1 make release-check
RUN_INTEGRATION=1 RUN_SOAK=1 RUN_SLOW_CLIENT=1 make release-check
```
Before publishing package recipes, download the final GitHub source archive,
Before publishing package recipes, download the explicit release source archive,
replace placeholder checksums, and run:
```sh
SOURCE_TARBALL=dist/tnt-chat-vX.Y.Z.tar.gz make package-publish-check
SOURCE_TARBALL=dist/tnt-chat-vX.Y.Z-source.tar.gz make package-publish-check
```
## Files

View file

@ -6,7 +6,12 @@
- Added a release tag/version guard used by the GitHub release workflow, so a
`vX.Y.Z` tag must match `TNT_VERSION` before release assets are built.
- Added `make package-publish-check` for verifying Arch/Homebrew source
checksums against the final GitHub source archive after a tag exists.
checksums against the explicit release source archive after a tag exists.
- Added a release artifact gate that bundles Linux/macOS binaries, the explicit
release source archive, and `checksums.txt` before opening the draft release.
- Added CI governance layers for fast PR checks, release-branch validation,
extended runtime validation, container portability builds, and package recipe
validation.
- Added a `config_defaults` module and unit coverage for runtime default
values, env keys, and accepted numeric ranges.
- Added a dedicated `tntctl_text` module with unit coverage for local
@ -53,7 +58,7 @@
matches, avoiding the impression that the pager is a complete result set.
- Release checks now separate tag/source-archive readiness from package-manager
checksum publishing, avoiding self-referential checksum requirements before
the final GitHub source archive exists.
the explicit release source archive exists.
- `tntctl --help` now gets its exec command list from `exec_catalog`, reducing
duplicate command metadata between the local wrapper and SSH exec mode.
- Updated `tnt(1)` to document the current TUI search and pager keys, and

View file

@ -1,171 +1,268 @@
CI / RELEASE GUIDE
==================
# CI/CD and Release Governance
AUTOMATIC TESTING
-----------------
Every push or PR automatically runs:
- Build on Ubuntu
- AddressSanitizer build
- `make ci-test` (strict integration, anonymous access, connection limits,
and security feature checks)
- Release/package preflight (`make release-check`)
TNT is a C SSH terminal chat server. The CI/CD system is designed for a public
open-source project: fast feedback on pull requests, broader scheduled
validation across target environments, reproducible release artifacts, and a
manual production deployment boundary.
Check status:
https://github.com/m1ngsama/TNT/actions
Production deployment is intentionally manual. Workflows must not SSH into
production, restart services, upload to OSS buckets, publish package-manager
recipes, or mutate running servers.
Production deployment is intentionally manual. The CI workflow must not SSH
into production or restart services on push.
## Pipeline Layers
### PR Fast Gate
CREATING RELEASES
-----------------
Release policy:
- Use SemVer-style tags: vMAJOR.MINOR.PATCH.
- Bump PATCH for compatible bug fixes and release hardening.
- Bump MINOR for new commands, new documented flags, JSON field additions,
or visible user-interface behavior changes.
- Bump MAJOR for incompatible command, config, storage, or package behavior.
- Keep GitHub draft release review manual. Do not auto-publish releases.
- Keep production deployment manual. Do not SSH into production from CI.
Workflow: `.github/workflows/ci.yml`
1. Update version metadata:
- include/common.h
- tnt.1
- docs/CHANGELOG.md
- packaging/arch/PKGBUILD
- packaging/homebrew/tnt-chat.rb
- packaging/debian/debian/changelog
- maintainer metadata, when preparing public package recipes
Runs on pull requests targeting `main` or `release/**`, and pushes to `main`
or `release/**`:
2. Run the local preflight:
make release-check
- Ubuntu 24.04 and macOS latest builds.
- Normal build with `make`.
- AddressSanitizer build with `make asan`.
- Integration/security gate with `make ci-test`.
- Local release/package preflight with `make release-check`.
For a longer local runtime gate before publishing or production rollout:
RUN_SOAK=1 RUN_SLOW_CLIENT=1 make release-check
Purpose:
3. Commit the release changes and create a local tag. Do not push the tag
until strict checks pass:
git tag vX.Y.Z
- Keep contributor feedback fast enough for normal review.
- Catch build, integration, packaging metadata, and release-preflight regressions
before merge.
- Avoid slow soak, valgrind, and container matrix jobs on every PR.
4. Run strict release checks:
make release-check-strict
### Extended and Nightly Validation
Strict mode requires the local `vX.Y.Z` tag to point at HEAD. It also
builds from the tagged source archive, so it catches files that were left
untracked and would be missing from GitHub's source archive.
Workflow: `.github/workflows/ci.yml`
5. Push the tag:
git push origin vX.Y.Z
Runs on `main` or `release/**` pushes, manual dispatch, and the nightly
schedule:
6. GitHub Actions automatically:
- Builds `tnt` and `tntctl` binaries (Linux/macOS, AMD64/ARM64)
- Creates a draft release
- Uploads binaries
- Generates one `checksums.txt` file
- Verifies that artifact architecture matches the asset name
- `extended-linux-runtime`
- Runs `RUN_INTEGRATION=1 RUN_SOAK=1 RUN_SLOW_CLIENT=1 make release-check`.
- Runs a valgrind smoke test against a temporary server.
- `portable-container-builds`
- Builds in Debian stable glibc.
- Builds in Ubuntu 24.04 glibc.
- Builds in Alpine musl.
- `package-recipe-gate`
- Syntax-checks shell scripts.
- Syntax-checks the Arch `PKGBUILD`.
- Syntax-checks the Homebrew formula.
- Assembles the Debian source tree.
7. Review the draft release, smoke-test downloaded assets, then publish it
manually from GitHub.
Purpose:
8. Release appears at:
https://github.com/m1ngsama/TNT/releases
- Broaden platform confidence without making every PR wait for the full matrix.
- Detect musl/glibc portability issues early.
- Keep package metadata reviewable before public registry submission.
### Release Artifact Gates
RELEASE REVIEW CHECKLIST
------------------------
Before publishing a draft release:
- Confirm `git tag` points at the intended commit.
- Download every release asset from GitHub, not from the local workspace.
- Verify downloaded assets against `checksums.txt` (`sha256sum -c
checksums.txt --ignore-missing` on Linux, or `shasum -a 256 -c` for each
downloaded asset on macOS).
- Run downloaded `tnt --version` and `tntctl --version`.
- Start a temporary server and check:
ssh -p 2222 server health
ssh -p 2222 server stats --json
ssh -p 2222 server users --json
ssh -p 2222 operator@server post "release smoke"
ssh -p 2222 server "tail -n 1"
- Check runtime dynamic links (`ldd` on Linux, `otool -L` on macOS) and make
sure `libssh` is documented for the target install path.
- Confirm `make release-check-strict` passed before pushing the tag.
- For package-manager recipes, download the final GitHub source archive,
replace Arch/Homebrew source checksums, then run:
SOURCE_TARBALL=dist/tnt-chat-vX.Y.Z.tar.gz make package-publish-check
Workflow: `.github/workflows/release.yml`
Runs only for SemVer tags matching `vMAJOR.MINOR.PATCH`:
ROLLBACK
--------
Production rollback stays manual:
1. Keep the previous binary before replacing it.
2. Stop or restart only the intended `tnt` service.
3. Restore the previous binary if smoke checks fail.
4. Re-run `health`, `stats --json`, and one post/tail smoke test.
- Verifies the tag matches `TNT_VERSION` through `scripts/check_release_ref.sh`.
- Builds Linux glibc AMD64 and ARM64 binaries.
- Builds macOS Intel and Apple Silicon binaries.
- Verifies binary architecture labels.
- Builds an explicit source archive: `tnt-chat-vX.Y.Z-source.tar.gz`.
- Runs `scripts/package_release_assets.sh` to collect release assets, verify
expected asset names, verify binary architecture labels again after artifact
download, verify source archive contents, generate `checksums.txt`, and verify
the checksum file.
- Creates a GitHub draft release only. Publishing stays manual.
Do not overwrite `TNT_STATE_DIR` during rollback. If a future release changes
the message log format, its release notes must include the downgrade behavior.
The release workflow does not publish package-manager recipes or deploy
production servers.
## Platform Policy
DEPLOYING TO SERVERS
--------------------
Deployments are operator-driven:
1. Build and test locally or in a temporary server directory.
2. Back up the installed binary.
3. Install the new binary.
4. Restart the service.
5. Run black-box checks (`health`, `stats --json`, `users --json`,
and a post/tail smoke test).
Current release assets:
The installer can still be used manually on a server:
curl -sSL https://raw.githubusercontent.com/m1ngsama/TNT/main/install.sh | sh
- Linux glibc AMD64: `tnt-linux-amd64`, `tntctl-linux-amd64`
- Linux glibc ARM64: `tnt-linux-arm64`, `tntctl-linux-arm64`
- macOS Intel: `tnt-darwin-amd64`, `tntctl-darwin-amd64`
- macOS Apple Silicon: `tnt-darwin-arm64`, `tntctl-darwin-arm64`
- Source archive: `tnt-chat-vX.Y.Z-source.tar.gz`
Current CI validation:
PRODUCTION SETUP (systemd)
---------------------------
1. Install binary (see above)
- Ubuntu 24.04
- macOS latest
- Debian stable glibc container build
- Ubuntu 24.04 glibc container build
- Alpine musl container build
2. Setup service:
sudo useradd -r -s /bin/false tnt
sudo mkdir -p /var/lib/tnt
sudo chown tnt:tnt /var/lib/tnt
sudo cp tnt.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now tnt
Package-manager routes:
3. Check status:
sudo systemctl status tnt
sudo journalctl -u tnt -f
- Debian/Ubuntu: maintain draft Debian metadata and start with a Launchpad PPA.
- Arch/AUR: maintain `packaging/arch/PKGBUILD` and `.SRCINFO`; submit manually.
- Homebrew/macOS: maintain a tap formula first; Homebrew core can wait for a
stable release cadence and broader adoption.
- Source archive: every public package recipe must pin the final GitHub release
source archive checksum.
- Containers: first stage is Docker-based build validation in CI. Publishing
images should wait until image labels, SBOM, provenance, CVE scanning, and
registry ownership are defined.
## Release Policy
UPDATING SERVERS
----------------
Manual binary replacement pattern:
backup=/usr/local/bin/tnt.bak-$(date +%Y%m%d%H%M%S)
sudo cp -a /usr/local/bin/tnt "$backup"
sudo install -m 755 ./tnt /usr/local/bin/tnt
sudo systemctl restart tnt
- Use SemVer-style tags: `vMAJOR.MINOR.PATCH`.
- Bump PATCH for compatible bug fixes and release hardening.
- Bump MINOR for new commands, new documented flags, JSON field additions, or
visible user-interface behavior changes.
- Bump MAJOR for incompatible command, config, storage, or package behavior.
- Keep GitHub release publishing manual by using draft releases.
- Keep production deployment manual.
Update version metadata before tagging:
PLATFORMS SUPPORTED
-------------------
✓ Linux AMD64 (x86_64)
✓ Linux ARM64 (aarch64)
✓ macOS Intel (x86_64)
✓ macOS Apple Silicon (arm64)
- `include/common.h`
- `tnt.1`
- `tntctl.1`
- `docs/CHANGELOG.md`
- `packaging/arch/PKGBUILD`
- `packaging/arch/.SRCINFO`
- `packaging/homebrew/tnt-chat.rb`
- `packaging/debian/debian/changelog`
Local preflight:
EXAMPLE WORKFLOW
----------------
# Local development
make && make asan && make release-check
./tnt
```sh
make release-check
```
# Create release
Longer local runtime gate:
```sh
RUN_INTEGRATION=1 RUN_SOAK=1 RUN_SLOW_CLIENT=1 make release-check
```
Strict local release gate before pushing a tag:
```sh
git tag vX.Y.Z
git push origin vX.Y.Z
# Wait 5 minutes for builds
make release-check-strict
```
# Deploy to production manually after validation
ssh server "sudo install -m 755 /tmp/tnt-build/tnt /usr/local/bin/tnt"
ssh server "sudo systemctl restart tnt"
ssh -p 2222 server health
Strict mode requires the local `vX.Y.Z` tag to point at `HEAD` and builds from
the tagged source archive, so it catches files that were left untracked and
would be missing from the release source archive.
After strict checks pass:
```sh
git push origin vX.Y.Z
```
GitHub Actions then builds artifacts and opens a draft release. Review and
publish that draft manually.
## Release Review Checklist
Before publishing a draft release:
- Confirm the Git tag points at the intended commit.
- Confirm the release workflow passed.
- Download every release asset from GitHub, not from the local workspace.
- Verify downloaded assets against `checksums.txt`.
- Run downloaded `tnt --version` and `tntctl --version`.
- Start a temporary server and check:
```sh
ssh -p 2222 server health
ssh -p 2222 server stats --json
ssh -p 2222 server users --json
ssh -p 2222 operator@server post "release smoke"
ssh -p 2222 server "tail -n 1"
```
- Check runtime dynamic links with `ldd` on Linux or `otool -L` on macOS.
- Confirm `libssh` runtime installation is documented for the target install
path.
- Verify the explicit source archive checksum before updating Arch, Homebrew,
Debian, Ubuntu, or container package metadata.
- Run package publication preflight after package recipes pin final source
checksums:
```sh
SOURCE_TARBALL=dist/tnt-chat-vX.Y.Z-source.tar.gz make package-publish-check
```
## Checksums
Release assets include `checksums.txt`.
Linux:
```sh
sha256sum -c checksums.txt --ignore-missing
```
macOS:
```sh
for f in tnt-* tntctl-* tnt-chat-*-source.tar.gz; do
grep " $f$" checksums.txt | shasum -a 256 -c -
done
```
## Supply Chain Roadmap
Stage 1, implemented now:
- Tag/version gate.
- Draft release, manual publish.
- Binary architecture validation.
- Source archive validation.
- SHA-256 checksums for every release asset.
- Package recipe checksum preflight.
Stage 2, next:
- Generate an SBOM for release artifacts, preferably CycloneDX or SPDX.
- Attach SBOM files to draft releases.
- Add package lint jobs for Debian source packages, Arch packages, Homebrew
audit, and container image metadata.
Stage 3, later:
- Sign release checksums and/or artifacts with a documented maintainer key or
Sigstore flow.
- Add SLSA provenance for GitHub Actions builds.
- Define container image ownership, tag policy, vulnerability scan policy, and
rollback behavior before publishing images.
## Manual Production Deployment
Deployment remains operator-driven:
1. Build and test locally or in a temporary server directory.
2. Back up the installed binary.
3. Install the new binary.
4. Restart only the intended `tnt` service.
5. Run black-box checks: `health`, `stats --json`, `users --json`, and one
post/tail smoke test.
Manual binary replacement pattern:
```sh
backup=/usr/local/bin/tnt.bak-$(date +%Y%m%d%H%M%S)
sudo cp -a /usr/local/bin/tnt "$backup"
sudo install -m 755 ./tnt /usr/local/bin/tnt
sudo systemctl restart tnt
```
## Rollback
Production rollback stays manual:
1. Keep the previous binary before replacing it.
2. Stop or restart only the intended `tnt` service.
3. Restore the previous binary if smoke checks fail.
4. Re-run `health`, `stats --json`, and one post/tail smoke test.
Do not overwrite `TNT_STATE_DIR` during rollback. If a future release changes
the message log format, its release notes must include downgrade behavior.

View file

@ -113,8 +113,8 @@ Goal: make regressions harder to introduce.
These are the next changes that should happen before new feature work expands the surface area.
1. Replace remaining source-archive checksum placeholders only after the final
GitHub source archive exists, then run `make package-publish-check`.
1. Replace remaining source-archive checksum placeholders only after the
explicit release source archive exists, then run `make package-publish-check`.
2. Create or move the `vX.Y.Z` tag only when the release commit is final, then
run `make release-check-strict` before pushing it.
3. Decide whether admin-only moderation controls belong in 1.0.x or should

View file

@ -13,12 +13,30 @@ any public registry.
Package installs include both `tnt` and `tntctl`. `tnt` is the server process;
`tntctl` is a thin wrapper around the documented SSH exec interface.
## CI governance
Package recipes are validated in stages:
- PR fast gate: `make release-check` verifies package metadata stays aligned
with `TNT_VERSION`.
- Extended CI: package syntax and Debian source-tree assembly run on `main` and
`release/**` pushes, nightly, and manual workflow dispatch.
- Release gate: the workflow builds an explicit release source archive, verifies
it, and includes it in `checksums.txt`.
- Publishing gate: after final source checksums are pinned, run
`SOURCE_TARBALL=... make package-publish-check`.
All package-manager submissions remain manual. CI must not push to AUR, open or
merge Homebrew tap updates, upload Debian/PPA packages, publish container
images, or deploy production servers.
## Release checklist
1. Confirm `TNT_VERSION` in `include/common.h` and the manpage version match.
Also update package versions in Arch, Homebrew, and Debian drafts.
2. Create a GitHub release tag such as `vX.Y.Z`.
3. Build and upload release tarballs or rely on GitHub source archives.
3. Let the release workflow build the explicit release source archive and draft
release assets.
4. Replace placeholder checksums in package drafts.
5. Verify package contents in an isolated directory:
@ -35,11 +53,11 @@ Package installs include both `tnt` and `tntctl`. `tnt` is the server process;
Use `scripts/package_debian_source.sh --build` on a Debian/Ubuntu system
with `dpkg-buildpackage` installed to build the unsigned source package.
7. Before submitting package recipes, download the final GitHub source archive,
7. Before submitting package recipes, download the explicit release source archive,
replace checksum placeholders, and run:
```sh
SOURCE_TARBALL=dist/tnt-chat-vX.Y.Z.tar.gz make package-publish-check
SOURCE_TARBALL=dist/tnt-chat-vX.Y.Z-source.tar.gz make package-publish-check
```
8. Submit packages manually:

View file

@ -9,7 +9,7 @@ pkgbase = tnt-chat
makedepends = gcc
makedepends = make
depends = libssh
source = tnt-chat-1.0.1.tar.gz::https://github.com/m1ngsama/TNT/archive/refs/tags/v1.0.1.tar.gz
source = tnt-chat-v1.0.1-source.tar.gz::https://github.com/m1ngsama/TNT/releases/download/v1.0.1/tnt-chat-v1.0.1-source.tar.gz
source = tnt-chat.sysusers
sha256sums = SKIP
sha256sums = 8a1f7dfbdc9f1305c4ed50d80e89f91333ffdf937890c497f93e41abaf76e3ed

View file

@ -9,7 +9,7 @@ url='https://github.com/m1ngsama/TNT'
license=('MIT')
depends=('libssh')
makedepends=('gcc' 'make')
source=("${pkgname}-${pkgver}.tar.gz::${url}/archive/refs/tags/v${pkgver}.tar.gz"
source=("${pkgname}-v${pkgver}-source.tar.gz::${url}/releases/download/v${pkgver}/${pkgname}-v${pkgver}-source.tar.gz"
"${pkgname}.sysusers")
sha256sums=('SKIP'
'8a1f7dfbdc9f1305c4ed50d80e89f91333ffdf937890c497f93e41abaf76e3ed')

View file

@ -26,12 +26,12 @@ After editing `PKGBUILD`, regenerate `.SRCINFO`:
makepkg --printsrcinfo > .SRCINFO
```
Before AUR submission, replace `sha256sums=('SKIP')` with the real GitHub
Before AUR submission, replace `sha256sums=('SKIP')` with the real release
source archive checksum, regenerate `.SRCINFO`, then run the package publish
check:
```sh
SOURCE_TARBALL=dist/tnt-chat-vX.Y.Z.tar.gz make package-publish-check
SOURCE_TARBALL=dist/tnt-chat-vX.Y.Z-source.tar.gz make package-publish-check
```
## Manual AUR submission

View file

@ -34,16 +34,16 @@ ruby -c packaging/homebrew/tnt-chat.rb
2. Download or hash the release source archive:
```sh
curl -L -o dist/tnt-chat-vX.Y.Z.tar.gz \
https://github.com/m1ngsama/TNT/archive/refs/tags/vX.Y.Z.tar.gz
shasum -a 256 dist/tnt-chat-vX.Y.Z.tar.gz
curl -L -o dist/tnt-chat-vX.Y.Z-source.tar.gz \
https://github.com/m1ngsama/TNT/releases/download/vX.Y.Z/tnt-chat-vX.Y.Z-source.tar.gz
shasum -a 256 dist/tnt-chat-vX.Y.Z-source.tar.gz
```
3. Replace `REPLACE_WITH_RELEASE_TARBALL_SHA256` in `tnt-chat.rb`.
4. Run:
```sh
SOURCE_TARBALL=dist/tnt-chat-vX.Y.Z.tar.gz make package-publish-check
SOURCE_TARBALL=dist/tnt-chat-vX.Y.Z-source.tar.gz make package-publish-check
```
5. Copy the formula into the tap repository and open a normal review PR.

View file

@ -1,7 +1,7 @@
class TntChat < Formula
desc "SSH-native terminal chat server with a Vim-style interface"
homepage "https://github.com/m1ngsama/TNT"
url "https://github.com/m1ngsama/TNT/archive/refs/tags/v1.0.1.tar.gz"
url "https://github.com/m1ngsama/TNT/releases/download/v1.0.1/tnt-chat-v1.0.1-source.tar.gz"
sha256 "REPLACE_WITH_RELEASE_TARBALL_SHA256"
license "MIT"

View file

@ -23,12 +23,21 @@ sha256_of() {
version=$(sed -n 's/^#define TNT_VERSION "\([^"]*\)".*/\1/p' include/common.h)
[ -n "$version" ] || fail "could not read TNT_VERSION from include/common.h"
release_source="tnt-chat-v${version}-source.tar.gz"
source_tarball=${SOURCE_TARBALL:-${RELEASE_SOURCE_TARBALL:-}}
[ -n "$source_tarball" ] ||
fail "set SOURCE_TARBALL to the final GitHub source archive"
fail "set SOURCE_TARBALL to the explicit release source archive"
[ -f "$source_tarball" ] ||
fail "SOURCE_TARBALL does not exist: $source_tarball"
tar -tzf "$source_tarball" >/dev/null ||
fail "SOURCE_TARBALL is not a readable tar.gz archive"
tar -tzf "$source_tarball" | grep -q "^TNT-$version/LICENSE$" ||
fail "SOURCE_TARBALL is missing LICENSE"
tar -tzf "$source_tarball" | grep -q "^TNT-$version/packaging/README.md$" ||
fail "SOURCE_TARBALL is missing packaging/README.md"
tar -tzf "$source_tarball" | grep -q "^TNT-$version/src/tntctl.c$" ||
fail "SOURCE_TARBALL is missing src/tntctl.c"
! grep -R "REPLACE_WITH_EMAIL" packaging/arch packaging/debian >/dev/null ||
fail "replace maintainer email placeholders before package publishing"
@ -60,8 +69,12 @@ grep -q "^pkgver=$version$" packaging/arch/PKGBUILD ||
fail "PKGBUILD pkgver does not match $version"
grep -q "pkgver = $version" packaging/arch/.SRCINFO ||
fail ".SRCINFO pkgver does not match $version"
grep -q "v${version}.tar.gz" packaging/homebrew/tnt-chat.rb ||
fail "Homebrew URL does not match v$version"
grep -q '${pkgname}-v${pkgver}-source.tar.gz' packaging/arch/PKGBUILD ||
fail "PKGBUILD source must use the release source archive"
grep -q "$release_source" packaging/arch/.SRCINFO ||
fail ".SRCINFO source does not match $release_source"
grep -q "$release_source" packaging/homebrew/tnt-chat.rb ||
fail "Homebrew URL does not match $release_source"
grep -q "^tnt-chat (${version}-1)" packaging/debian/debian/changelog ||
fail "Debian changelog version does not match $version"

150
scripts/package_release_assets.sh Executable file
View file

@ -0,0 +1,150 @@
#!/bin/sh
# Collect release workflow artifacts into one flat, checksum-verified bundle.
set -eu
usage() {
cat <<'USAGE'
Usage: scripts/package_release_assets.sh ARTIFACT_ROOT [OUT_DIR]
ARTIFACT_ROOT is the directory produced by actions/download-artifact.
OUT_DIR defaults to dist/release-assets.
The script validates the expected release asset names, verifies binary
architecture labels, verifies the source archive shape, writes checksums.txt,
and verifies that checksums.txt matches the assembled bundle. It never
publishes a release.
USAGE
}
fail() {
echo "release-artifact-gate: $*" >&2
exit 1
}
sha256_of() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$1" | awk '{print $1}'
elif command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$1" | awk '{print $1}'
else
fail "sha256sum or shasum is required"
fi
}
find_unique() {
name=$1
matches=$(find "$ARTIFACT_ROOT" -type f -name "$name" -print)
count=$(printf '%s\n' "$matches" | sed '/^$/d' | wc -l | tr -d ' ')
[ "$count" = "1" ] ||
fail "expected exactly one artifact named $name, found $count"
printf '%s\n' "$matches"
}
require_file_label() {
path=$1
pattern=$2
label=$(file "$path")
printf '%s\n' "$label" | grep -E "$pattern" >/dev/null ||
fail "unexpected file type for $(basename "$path"): $label"
}
verify_asset() {
name=$1
path=$2
[ -s "$path" ] || fail "empty artifact: $name"
case "$name" in
tnt-linux-amd64|tntctl-linux-amd64)
require_file_label "$path" 'ELF 64-bit.*x86-64'
;;
tnt-linux-arm64|tntctl-linux-arm64)
require_file_label "$path" 'ELF 64-bit.*(aarch64|ARM aarch64)'
;;
tnt-darwin-amd64|tntctl-darwin-amd64)
require_file_label "$path" 'Mach-O 64-bit.*x86_64'
;;
tnt-darwin-arm64|tntctl-darwin-arm64)
require_file_label "$path" 'Mach-O 64-bit.*arm64'
;;
tnt-chat-v*-source.tar.gz)
tar -tzf "$path" >/dev/null
tar -tzf "$path" | grep -q "^TNT-$VERSION/LICENSE$" ||
fail "source archive is missing LICENSE"
tar -tzf "$path" | grep -q "^TNT-$VERSION/src/tntctl.c$" ||
fail "source archive is missing src/tntctl.c"
tar -tzf "$path" | grep -q "^TNT-$VERSION/packaging/README.md$" ||
fail "source archive is missing packaging/README.md"
;;
*)
fail "unexpected release artifact: $name"
;;
esac
}
[ "${1:-}" != "-h" ] && [ "${1:-}" != "--help" ] || {
usage
exit 0
}
ARTIFACT_ROOT=${1:-}
OUT_DIR=${2:-dist/release-assets}
[ -n "$ARTIFACT_ROOT" ] || {
usage >&2
exit 2
}
[ -d "$ARTIFACT_ROOT" ] || fail "ARTIFACT_ROOT does not exist: $ARTIFACT_ROOT"
ARTIFACT_ROOT=$(CDPATH= cd -- "$ARTIFACT_ROOT" && pwd)
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
cd "$ROOT"
case "$OUT_DIR" in
/*) ;;
*) OUT_DIR="$ROOT/$OUT_DIR" ;;
esac
VERSION=$(sed -n 's/^#define TNT_VERSION "\([^"]*\)".*/\1/p' include/common.h)
[ -n "$VERSION" ] || fail "could not read TNT_VERSION"
SOURCE_ASSET="tnt-chat-v$VERSION-source.tar.gz"
EXPECTED_ASSETS="
tnt-linux-amd64
tntctl-linux-amd64
tnt-linux-arm64
tntctl-linux-arm64
tnt-darwin-amd64
tntctl-darwin-amd64
tnt-darwin-arm64
tntctl-darwin-arm64
$SOURCE_ASSET
"
mkdir -p "$OUT_DIR"
for name in $EXPECTED_ASSETS; do
dst="$OUT_DIR/$name"
[ ! -e "$dst" ] || fail "output already exists: $dst"
src=$(find_unique "$name")
verify_asset "$name" "$src"
cp "$src" "$dst"
done
(
cd "$OUT_DIR"
: > checksums.txt
for name in $EXPECTED_ASSETS; do
printf '%s %s\n' "$(sha256_of "$name")" "$name" >> checksums.txt
done
while read -r expected name; do
[ -n "$expected" ] || continue
actual=$(sha256_of "$name")
[ "$actual" = "$expected" ] ||
fail "checksum mismatch for $name"
done < checksums.txt
)
echo "release artifact bundle ready: $OUT_DIR"

View file

@ -28,7 +28,7 @@ Environment:
Strict checks additionally require a clean tree, a vX.Y.Z tag at HEAD, a
matching changelog release section, non-placeholder maintainer metadata, and a
build from the tagged source archive. Run `make package-publish-check` after
the final GitHub source archive exists to verify package checksums.
the explicit release source archive exists to verify package checksums.
USAGE
}
@ -78,8 +78,12 @@ grep -q "^pkgname=tnt-chat$" packaging/arch/PKGBUILD ||
fail "packaging/arch/PKGBUILD pkgname is not tnt-chat"
grep -q "^pkgname = tnt-chat$" packaging/arch/.SRCINFO ||
fail "packaging/arch/.SRCINFO pkgname is not tnt-chat"
grep -q "v${version}.tar.gz" packaging/homebrew/tnt-chat.rb ||
fail "packaging/homebrew/tnt-chat.rb URL does not match v$version"
grep -q '${pkgname}-v${pkgver}-source.tar.gz' packaging/arch/PKGBUILD ||
fail "packaging/arch/PKGBUILD source must use the release source archive"
grep -q "tnt-chat-v${version}-source.tar.gz" packaging/arch/.SRCINFO ||
fail "packaging/arch/.SRCINFO source does not match v$version release archive"
grep -q "tnt-chat-v${version}-source.tar.gz" packaging/homebrew/tnt-chat.rb ||
fail "packaging/homebrew/tnt-chat.rb URL does not match v$version release archive"
grep -q "^class TntChat < Formula$" packaging/homebrew/tnt-chat.rb ||
fail "packaging/homebrew/tnt-chat.rb formula class is not TntChat"
grep -q 'depends_on "libssh"' packaging/homebrew/tnt-chat.rb ||
@ -193,6 +197,7 @@ step "checking installer syntax"
sh -n install.sh
sh -n scripts/check_release_ref.sh
sh -n scripts/package_publish_check.sh
sh -n scripts/package_release_assets.sh
scripts/check_release_ref.sh "v$version"
bad_ref=v0.0.0
[ "$version" != "0.0.0" ] || bad_ref=v9.9.9

View file

@ -0,0 +1,158 @@
#!/bin/sh
# Release-artifact gate regression tests.
set -u
PASS=0
FAIL=0
SCRIPT="../scripts/package_release_assets.sh"
STATE_DIR=$(mktemp -d "${TMPDIR:-/tmp}/tnt-release-artifact-test.XXXXXX")
cleanup() {
rm -rf "$STATE_DIR"
}
trap cleanup EXIT
pass() {
echo "$1"
PASS=$((PASS + 1))
}
fail() {
echo "$1"
if [ "${2:-}" ]; then
printf '%s\n' "$2"
fi
FAIL=$((FAIL + 1))
}
version() {
sed -n 's/^#define TNT_VERSION "\([^"]*\)".*/\1/p' ../include/common.h
}
write_elf_x86_64() {
printf '\177ELF\002\001\001\000\000\000\000\000\000\000\000\000\002\000\076\000\001\000\000\000' > "$1"
}
write_elf_aarch64() {
printf '\177ELF\002\001\001\000\000\000\000\000\000\000\000\000\002\000\267\000\001\000\000\000' > "$1"
}
write_macho_x86_64() {
printf '\317\372\355\376\007\000\000\001\003\000\000\000\002\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000' > "$1"
}
write_macho_arm64() {
printf '\317\372\355\376\014\000\000\001\000\000\000\000\002\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000' > "$1"
}
build_artifact_tree() {
artifact_root=$1
include_source=$2
ver=$(version)
mkdir -p \
"$artifact_root/linux-amd64" \
"$artifact_root/linux-arm64" \
"$artifact_root/darwin-amd64" \
"$artifact_root/darwin-arm64"
write_elf_x86_64 "$artifact_root/linux-amd64/tnt-linux-amd64"
write_elf_x86_64 "$artifact_root/linux-amd64/tntctl-linux-amd64"
write_elf_aarch64 "$artifact_root/linux-arm64/tnt-linux-arm64"
write_elf_aarch64 "$artifact_root/linux-arm64/tntctl-linux-arm64"
write_macho_x86_64 "$artifact_root/darwin-amd64/tnt-darwin-amd64"
write_macho_x86_64 "$artifact_root/darwin-amd64/tntctl-darwin-amd64"
write_macho_arm64 "$artifact_root/darwin-arm64/tnt-darwin-arm64"
write_macho_arm64 "$artifact_root/darwin-arm64/tntctl-darwin-arm64"
if [ "$include_source" = "yes" ]; then
source_root="$STATE_DIR/source-$ver/TNT-$ver"
mkdir -p "$source_root/src" "$source_root/packaging" "$artifact_root/source"
printf 'MIT\n' > "$source_root/LICENSE"
printf 'int main(void) { return 0; }\n' > "$source_root/src/tntctl.c"
printf '# Packaging\n' > "$source_root/packaging/README.md"
(cd "$STATE_DIR/source-$ver" && tar -czf "$artifact_root/source/tnt-chat-v$ver-source.tar.gz" "TNT-$ver")
fi
}
expect_file() {
path=$1
name=$2
[ -f "$path" ] && pass "$name" || fail "$name missing"
}
echo "=== TNT Release Artifact Gate Tests ==="
if [ ! -x "$SCRIPT" ]; then
echo "Error: script $SCRIPT not found or not executable."
exit 1
fi
VER=$(version)
ARTIFACT_ROOT="$STATE_DIR/artifacts"
OUT_DIR="$STATE_DIR/out"
build_artifact_tree "$ARTIFACT_ROOT" yes
OUTPUT=$("$SCRIPT" "$ARTIFACT_ROOT" "$OUT_DIR" 2>&1)
STATUS=$?
if [ "$STATUS" -eq 0 ] &&
printf '%s\n' "$OUTPUT" | grep -q 'release artifact bundle ready'; then
pass "complete artifact set is accepted"
else
fail "complete artifact set failed" "$OUTPUT"
fi
for asset in \
tnt-linux-amd64 \
tntctl-linux-amd64 \
tnt-linux-arm64 \
tntctl-linux-arm64 \
tnt-darwin-amd64 \
tntctl-darwin-amd64 \
tnt-darwin-arm64 \
tntctl-darwin-arm64 \
"tnt-chat-v$VER-source.tar.gz" \
checksums.txt
do
expect_file "$OUT_DIR/$asset" "bundles $asset"
done
if grep -q " tnt-linux-amd64$" "$OUT_DIR/checksums.txt" &&
grep -q " tnt-chat-v$VER-source.tar.gz$" "$OUT_DIR/checksums.txt"; then
pass "checksums include binaries and source archive"
else
fail "checksums are incomplete" "$(cat "$OUT_DIR/checksums.txt" 2>/dev/null)"
fi
DUP_ROOT="$STATE_DIR/duplicate"
DUP_OUT="$STATE_DIR/duplicate-out"
build_artifact_tree "$DUP_ROOT" yes
mkdir -p "$DUP_ROOT/duplicate"
cp "$DUP_ROOT/linux-amd64/tnt-linux-amd64" "$DUP_ROOT/duplicate/tnt-linux-amd64"
DUP_OUTPUT=$("$SCRIPT" "$DUP_ROOT" "$DUP_OUT" 2>&1)
DUP_STATUS=$?
if [ "$DUP_STATUS" -ne 0 ] &&
printf '%s\n' "$DUP_OUTPUT" | grep -q 'expected exactly one artifact named tnt-linux-amd64'; then
pass "duplicate artifact is rejected"
else
fail "duplicate artifact handling" "$DUP_OUTPUT"
fi
MISSING_ROOT="$STATE_DIR/missing"
MISSING_OUT="$STATE_DIR/missing-out"
build_artifact_tree "$MISSING_ROOT" no
MISSING_OUTPUT=$("$SCRIPT" "$MISSING_ROOT" "$MISSING_OUT" 2>&1)
MISSING_STATUS=$?
if [ "$MISSING_STATUS" -ne 0 ] &&
printf '%s\n' "$MISSING_OUTPUT" | grep -q "expected exactly one artifact named tnt-chat-v$VER-source.tar.gz"; then
pass "missing source archive is rejected"
else
fail "missing source archive handling" "$MISSING_OUTPUT"
fi
echo ""
echo "PASSED: $PASS"
echo "FAILED: $FAIL"
[ "$FAIL" -eq 0 ] && echo "All tests passed" || echo "Some tests failed"
exit "$FAIL"

View file

@ -106,7 +106,7 @@ else
fi
IDLE_READY="$STATE_DIR/idle.ready"
IDLE_HOLD=$((DURATION + 2))
IDLE_HOLD=$((DURATION + 20))
cat >"$STATE_DIR/idle.expect" <<EOF
set timeout [expr {$IDLE_HOLD + 20}]
spawn ssh $SSH_TTY_OPTS idle@127.0.0.1