Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 18 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,32 @@ permissions:
contents: read

jobs:
test:
check:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Set up Go
uses: actions/setup-go@v6
- name: Setup Go
uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true

- name: Check formatting
run: test -z "$(gofmt -l .)"

- name: Vet
run: go vet ./...
- name: Setup Bats
uses: bats-core/bats-action@3.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Test
run: go test -race ./...
- name: Validate release configuration
uses: goreleaser/goreleaser-action@v7
with:
distribution: goreleaser
version: "~> v2"
args: check

- name: Build
run: go build ./cmd/termcourse
- name: Run checks
run: make check
108 changes: 108 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
name: Release

on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: Existing semantic-version tag to publish
required: true
type: string

permissions: {}

concurrency:
group: release-${{ inputs.tag || github.ref_name }}
cancel-in-progress: false

jobs:
validate:
name: Validate and test
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
outputs:
commit: ${{ steps.release_commit.outputs.sha }}

steps:
- name: Checkout release tag
uses: actions/checkout@v7
with:
fetch-depth: 0
ref: ${{ inputs.tag || github.ref }}

- name: Validate release tag
id: release_commit
env:
RELEASE_TAG: ${{ inputs.tag || github.ref_name }}
run: |
if [[ ! "$RELEASE_TAG" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
echo "Release tag must be semantic and start with v: $RELEASE_TAG" >&2
exit 1
fi
git show-ref --verify --quiet "refs/tags/$RELEASE_TAG"
git fetch --no-tags origin master
if ! git merge-base --is-ancestor "$RELEASE_TAG^{commit}" FETCH_HEAD; then
echo "Release tag is not reachable from origin/master: $RELEASE_TAG" >&2
exit 1
fi
printf 'sha=%s\n' "$(git rev-parse "$RELEASE_TAG^{commit}")" >> "$GITHUB_OUTPUT"

- name: Setup Go
uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true

- name: Setup Bats
uses: bats-core/bats-action@3.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Run checks
run: make check

release:
name: Build and publish
needs: validate
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: write

steps:
- name: Checkout release tag
uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ inputs.tag || github.ref }}

- name: Verify validated commit
env:
EXPECTED_COMMIT: ${{ needs.validate.outputs.commit }}
RELEASE_TAG: ${{ inputs.tag || github.ref_name }}
run: |
actual_commit=$(git rev-parse "$RELEASE_TAG^{commit}")
if [[ "$actual_commit" != "$EXPECTED_COMMIT" ]]; then
echo "Release tag changed after validation: $RELEASE_TAG" >&2
exit 1
fi

- name: Setup Go
uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true

- name: Build and publish GitHub release
uses: goreleaser/goreleaser-action@v7
with:
distribution: goreleaser
version: "~> v2"
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35 changes: 35 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
version: 2

project_name: termcourse

builds:
- id: termcourse
main: ./cmd/termcourse
binary: termcourse
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm64
flags:
- -buildvcs=false
- -trimpath
ldflags:
- -s -w -X github.com/merefield/termcourse.buildVersion={{ .Version }}

archives:
- formats: [tar.gz]
format_overrides:
- goos: windows
formats: [zip]
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"

checksum:
name_template: checksums.txt

changelog:
sort: asc
30 changes: 27 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,16 +1,40 @@
.PHONY: build test fmt vet
SHELL := /bin/bash
BINARY := termcourse
OUTPUT ?= $(BINARY)
PACKAGE := ./cmd/termcourse

VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo v0.2.0)
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo v0.2.1)
LDFLAGS := -X github.com/merefield/termcourse.buildVersion=$(VERSION)

.PHONY: build test race-test fmt fmt-check vet integration-test check install clean

build:
go build -ldflags "$(LDFLAGS)" ./cmd/termcourse
go build -buildvcs=false -trimpath -ldflags "$(LDFLAGS)" -o $(OUTPUT) $(PACKAGE)

test:
go test ./...

race-test:
go test -race ./...

fmt:
gofmt -w .

fmt-check:
test -z "$$(gofmt -l .)"

vet:
go vet ./...

integration-test:
bats test

check: fmt-check vet race-test integration-test build

install: build
mkdir -p $${DESTDIR}$${PREFIX:-/usr/local}/bin
install -m 0755 $(OUTPUT) $${DESTDIR}$${PREFIX:-/usr/local}/bin/$(BINARY)

clean:
go clean
rm -f $(OUTPUT)
79 changes: 69 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# termcourse

[![Version](https://img.shields.io/github/v/tag/merefield/termcourse?sort=semver&label=version)](https://github.com/merefield/termcourse/tags)
[![Latest release](https://img.shields.io/github/v/release/merefield/termcourse?display_name=tag&sort=semver&label=release)](https://github.com/merefield/termcourse/releases/latest)
[![CI](https://github.com/merefield/termcourse/actions/workflows/ci.yml/badge.svg)](https://github.com/merefield/termcourse/actions/workflows/ci.yml)
[![Go version](https://img.shields.io/github/go-mod/go-version/merefield/termcourse)](go.mod)
[![License](https://img.shields.io/github/license/merefield/termcourse)](LICENSE)
Expand Down Expand Up @@ -34,19 +34,55 @@ Termcourse is a Go 1.26.6 terminal UI for browsing and posting to Discourse foru

## Install and run

Go 1.26.6 or newer is required when installing from source. The shortest installation path is:
### Prebuilt release (recommended)

On Linux or macOS, the release installer selects the archive for the current operating system and architecture, verifies its SHA-256 checksum, checks the binary's reported version, and installs it as `/usr/local/bin/termcourse`:

```sh
curl -fsSL https://raw.githubusercontent.com/merefield/termcourse/master/install-release.sh | sh
```

The installer requires `curl` or `wget`, `tar`, and either `sha256sum` (Linux) or `shasum` (macOS). It uses `sudo` only when the destination is not writable. For a user-local installation that does not require `sudo`:

```sh
curl -fsSL https://raw.githubusercontent.com/merefield/termcourse/master/install-release.sh |
TERMCOURSE_BIN_DIR="$HOME/.local/bin" sh
```

Ensure `$HOME/.local/bin` is on `PATH` when using that location. To install a particular release reproducibly:

```sh
curl -fsSL https://raw.githubusercontent.com/merefield/termcourse/master/install-release.sh |
sh -s -- --version v0.2.1
```

You can download and inspect [install-release.sh](install-release.sh) before running it. The installer supports `--help`, `--version TAG`, and `--bin-dir DIR`; the equivalent environment variables are `TERMCOURSE_VERSION` and `TERMCOURSE_BIN_DIR`.

Prebuilt releases do not require Go. Each [GitHub Release](https://github.com/merefield/termcourse/releases) contains these assets:

| Operating system | Architectures | Archive | Installation |
| --- | --- | --- | --- |
| Linux | AMD64, ARM64 | `.tar.gz` | Installer or manual |
| macOS | Intel (AMD64), Apple Silicon (ARM64) | `.tar.gz` | Installer or manual |
| Windows | AMD64, ARM64 | `.zip` | Manual |

For a manual installation, verify the selected archive against the release's `checksums.txt`, extract `termcourse` (or `termcourse.exe` on Windows), and place it on `PATH`.

Run Termcourse with the hostname or URL of any Discourse site:

```sh
go install github.com/merefield/termcourse/cmd/termcourse@latest
termcourse meta.discourse.org
```

Replace `meta.discourse.org` with the hostname or URL of any Discourse site. If no credentials are configured, Termcourse prompts for the missing username and password. Password input is hidden.
If no credentials are configured, Termcourse prompts for the missing username and password. Password input is hidden. Confirm the installed release at any time with `termcourse --version`.

Confirm the installed release at any time with:
### Install with Go

Go 1.26.6 or newer is required when installing from source. `go install` compiles Termcourse locally:

```sh
termcourse --version
go install github.com/merefield/termcourse/cmd/termcourse@latest
termcourse meta.discourse.org
```

If the shell cannot find `termcourse`, add the Go binary directory to `PATH`. `go install` uses `GOBIN` when configured and otherwise uses `$(go env GOPATH)/bin`:
Expand All @@ -55,7 +91,7 @@ If the shell cannot find `termcourse`, add the Go binary directory to `PATH`. `g
export PATH="$(go env GOPATH)/bin:$PATH"
```

To build a local executable instead:
### Build from a checkout

```sh
git clone https://github.com/merefield/termcourse.git
Expand All @@ -70,6 +106,12 @@ make build
go run ./cmd/termcourse meta.discourse.org
```

`make install` is also available and honours `DESTDIR` and `PREFIX`:

```sh
make install PREFIX="$HOME/.local"
```

For repeat use, credentials can be supplied in `.env` or the host-mapped credentials file described under [Configuration](#configuration). The examples below use an installed `termcourse`; replace it with `./termcourse` when running a binary built in the repository. Username/password login enables realtime MessageBus updates:

```sh
Expand All @@ -95,11 +137,28 @@ termcourse themes hacker

A local `.env` is loaded automatically. CLI credentials override host credentials from YAML, which override generic environment variables. If both login and API pairs exist, login is tried first unless the host entry selects `auth: api`.

For contributors, run the local checks with `make test` and `make vet`.
For contributors, `make check` runs formatting validation, vet, race-enabled Go tests, installer integration tests, and a local build.

## Releases and versioning

Termcourse uses semantic Git tags such as `v0.2.1` as the release-version source of truth. Go embeds that module version in binaries installed with `go install`; GoReleaser injects it into release binaries; and `make build` injects the current `git describe` value. `termcourse --version` and the wide masthead subtitle use the same resolved build version. Untagged direct development builds append their embedded commit and dirty state to the development version declared in [termcourse.go](termcourse.go).

## Versioning
[GoReleaser](.goreleaser.yaml) builds static Linux, macOS, and Windows archives for AMD64 and ARM64, plus `checksums.txt`. Test the configuration locally without publishing:

```sh
goreleaser release --snapshot --clean --skip=publish
```

Pushing a semantic-version tag runs [the release workflow](.github/workflows/release.yml). It validates the tag syntax and confirms the tagged commit is reachable from `master`, runs the complete check suite, verifies that the tag did not move between validation and publication, and then creates the GitHub Release. No package manager, container registry, or announcement publisher is configured.

After this release workflow reaches `master`, create `v0.2.1` from the intended release commit. The existing `v0.2.0` tag remains immutable and has no generated binary release:

```sh
git tag -a v0.2.1 -m "termcourse v0.2.1"
git push origin v0.2.1
```

Termcourse uses semantic Git tags such as `v0.2.0` as the release-version source of truth. Go embeds that module version in binaries installed with `go install`, while `make build` injects the current `git describe` value. `termcourse --version` and the wide masthead subtitle both use the same resolved build version. Untagged direct development builds append their embedded commit and dirty state to the development version declared in [termcourse.go](termcourse.go).
An existing unpublished tag containing the release configuration can also be published explicitly with `gh workflow run release.yml --ref master -f tag=TAG`.

## Migrating from the Ruby version

Expand Down
Loading
Loading