Compare commits
2 Commits
cc83802244
...
0a02f0fc60
| Author | SHA1 | Date |
|---|---|---|
|
|
0a02f0fc60 | |
|
|
19ffc4ba5e |
|
|
@ -0,0 +1,71 @@
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ci-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: test (${{ matrix.os }})
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
# Formatting is platform independent, so check it once rather than twice.
|
||||||
|
- name: gofmt
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
run: |
|
||||||
|
unformatted="$(gofmt -l .)"
|
||||||
|
if [ -n "$unformatted" ]; then
|
||||||
|
echo "These files are not gofmt-clean:" >&2
|
||||||
|
echo "$unformatted" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: go vet
|
||||||
|
run: go vet ./...
|
||||||
|
|
||||||
|
- name: go test
|
||||||
|
run: go test ./... -count=1
|
||||||
|
|
||||||
|
cross-build:
|
||||||
|
name: cross-build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
# Mirrors the release targets, so a platform-specific break surfaces on
|
||||||
|
# the pull request rather than at tag time.
|
||||||
|
- name: build all release targets
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64; do
|
||||||
|
goos="${target%/*}"
|
||||||
|
goarch="${target#*/}"
|
||||||
|
echo "==> ${goos}/${goarch}"
|
||||||
|
GOOS="$goos" GOARCH="$goarch" CGO_ENABLED=0 \
|
||||||
|
go build -trimpath -o /tmp/sshkeeper-ci-build .
|
||||||
|
done
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
name: Nightly
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
# Two pushes in quick succession must not race for the rolling tag. Let the
|
||||||
|
# newer commit win rather than publishing a nightly built from older code.
|
||||||
|
concurrency:
|
||||||
|
group: nightly
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
nightly:
|
||||||
|
name: publish nightly
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
# Cheaper than the full release-check, but still refuses to publish a
|
||||||
|
# broken build.
|
||||||
|
- name: test
|
||||||
|
run: |
|
||||||
|
go vet ./...
|
||||||
|
go test ./... -count=1
|
||||||
|
|
||||||
|
# Version discovery is pinned to v* tags (see build.sh), so the rolling
|
||||||
|
# nightly tag below cannot hijack this value.
|
||||||
|
- name: resolve version
|
||||||
|
id: version
|
||||||
|
run: echo "value=$(git describe --tags --match 'v*' --always)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: build artifacts
|
||||||
|
env:
|
||||||
|
VERSION: ${{ steps.version.outputs.value }}
|
||||||
|
run: ./release.sh "$VERSION"
|
||||||
|
|
||||||
|
# Move the rolling tag before touching the release: a GitHub release must
|
||||||
|
# point at a tag, and this one always tracks the tip of main.
|
||||||
|
- name: move nightly tag
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
git tag -f nightly
|
||||||
|
git push -f origin nightly
|
||||||
|
|
||||||
|
# Replace rather than update: assets are immutable once uploaded, so the
|
||||||
|
# old release has to go before the new archives can take its name.
|
||||||
|
- name: replace nightly release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
VERSION: ${{ steps.version.outputs.value }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Heredoc, not an inline string: the notes are markdown and must not
|
||||||
|
# inherit this file's YAML indentation.
|
||||||
|
cat > /tmp/nightly-notes.md <<EOF
|
||||||
|
Automated build from the tip of \`main\`, rebuilt on every push.
|
||||||
|
|
||||||
|
**This is not a stable release.** It is untagged, unannounced and may be
|
||||||
|
broken. The \`Latest\` badge stays on the newest \`v*\` release, which is
|
||||||
|
what you want for normal use.
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Version | \`${VERSION}\` |
|
||||||
|
| Commit | ${GITHUB_SHA} |
|
||||||
|
| Built | $(date -u '+%Y-%m-%d %H:%M UTC') |
|
||||||
|
|
||||||
|
Verify downloads against \`checksums.txt\`.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
gh release delete nightly --yes || echo "no previous nightly release"
|
||||||
|
gh release create nightly \
|
||||||
|
--prerelease \
|
||||||
|
--title "sshkeeper nightly (${VERSION})" \
|
||||||
|
--notes-file /tmp/nightly-notes.md \
|
||||||
|
"dist/sshkeeper_${VERSION}_linux_amd64.tar.gz" \
|
||||||
|
"dist/sshkeeper_${VERSION}_linux_arm64.tar.gz" \
|
||||||
|
"dist/sshkeeper_${VERSION}_darwin_amd64.tar.gz" \
|
||||||
|
"dist/sshkeeper_${VERSION}_darwin_arm64.tar.gz" \
|
||||||
|
"dist/sshkeeper_${VERSION}_windows_amd64.zip" \
|
||||||
|
dist/checksums.txt
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ['v*']
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: publish ${{ github.ref_name }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
# Full history and tags: release.sh derives SOURCE_DATE_EPOCH from the
|
||||||
|
# tagged commit, and version discovery needs the v* tags to be present.
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
# Gate the release on the same checks used locally. A red suite must not
|
||||||
|
# be able to publish.
|
||||||
|
- name: release checks
|
||||||
|
run: make release-check
|
||||||
|
|
||||||
|
# Build through release.sh rather than reimplementing packaging here, so
|
||||||
|
# CI and a local ./release.sh produce byte-identical archives.
|
||||||
|
- name: build artifacts
|
||||||
|
env:
|
||||||
|
VERSION: ${{ github.ref_name }}
|
||||||
|
run: ./release.sh "$VERSION"
|
||||||
|
|
||||||
|
- name: publish
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
VERSION: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# A hand-written docs/releases/<tag>.md wins; otherwise fall back to
|
||||||
|
# GitHub's generated changelog.
|
||||||
|
notes="docs/releases/${VERSION}.md"
|
||||||
|
if [ -f "$notes" ]; then
|
||||||
|
echo "Using hand-written notes from $notes"
|
||||||
|
set -- --notes-file "$notes"
|
||||||
|
else
|
||||||
|
echo "No $notes, generating notes from commit history"
|
||||||
|
set -- --generate-notes
|
||||||
|
fi
|
||||||
|
|
||||||
|
gh release create "$VERSION" \
|
||||||
|
--title "sshkeeper $VERSION" \
|
||||||
|
--verify-tag \
|
||||||
|
"$@" \
|
||||||
|
"dist/sshkeeper_${VERSION}_linux_amd64.tar.gz" \
|
||||||
|
"dist/sshkeeper_${VERSION}_linux_arm64.tar.gz" \
|
||||||
|
"dist/sshkeeper_${VERSION}_darwin_amd64.tar.gz" \
|
||||||
|
"dist/sshkeeper_${VERSION}_darwin_arm64.tar.gz" \
|
||||||
|
"dist/sshkeeper_${VERSION}_windows_amd64.zip" \
|
||||||
|
dist/checksums.txt
|
||||||
5
build.sh
5
build.sh
|
|
@ -4,7 +4,10 @@ set -euo pipefail
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
APP=sshkeeper
|
APP=sshkeeper
|
||||||
VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
# --match 'v*' keeps the rolling `nightly` tag from hijacking the version: a
|
||||||
|
# plain `git describe --tags` picks whichever tag is nearest, so a nightly build
|
||||||
|
# would otherwise stamp binaries "nightly" instead of v<last release>-N-g<sha>.
|
||||||
|
VERSION=$(git describe --tags --match 'v*' --always --dirty 2>/dev/null || echo "dev")
|
||||||
LDFLAGS="-s -w -X main.version=${VERSION}"
|
LDFLAGS="-s -w -X main.version=${VERSION}"
|
||||||
|
|
||||||
echo "==> Building ${APP} ${VERSION}..."
|
echo "==> Building ${APP} ${VERSION}..."
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,40 @@
|
||||||
# Release Packaging
|
# Release Packaging
|
||||||
|
|
||||||
This document describes the manual release flow for sshkeeper.
|
Releases are published by GitHub Actions. Pushing a `v*` tag is the whole
|
||||||
|
release procedure; the rest of this document describes what that automation
|
||||||
|
runs, and how to reproduce it by hand when needed.
|
||||||
|
|
||||||
|
## Workflows
|
||||||
|
|
||||||
|
| Workflow | Trigger | Result |
|
||||||
|
|----------|---------|--------|
|
||||||
|
| `ci.yml` | push to `main`, every pull request | `gofmt`, `go vet`, `go test` on Linux and macOS, plus a cross-build of all five release targets |
|
||||||
|
| `release.yml` | push of a `v*` tag | runs `make release-check`, then `release.sh`, then publishes the GitHub release |
|
||||||
|
| `nightly.yml` | push to `main` | rebuilds the tip of `main` and replaces the `nightly` prerelease |
|
||||||
|
|
||||||
|
`release.yml` builds through `release.sh` rather than reimplementing packaging,
|
||||||
|
so a local run produces byte-identical archives.
|
||||||
|
|
||||||
|
### Release notes
|
||||||
|
|
||||||
|
`release.yml` looks for `docs/releases/<tag>.md`. If that file exists it becomes
|
||||||
|
the release body; otherwise GitHub generates notes from commit history. Write
|
||||||
|
the file before pushing the tag when a release deserves a real description.
|
||||||
|
|
||||||
|
### The nightly prerelease
|
||||||
|
|
||||||
|
`nightly.yml` force-moves a rolling `nightly` tag to the tip of `main` and
|
||||||
|
republishes a prerelease from it. It is marked prerelease deliberately, so
|
||||||
|
GitHub's `Latest` badge stays on the newest `v*` release.
|
||||||
|
|
||||||
|
Because that tag moves, version discovery in `build.sh` and `release.sh` is
|
||||||
|
pinned with `--match 'v*'`. Without the filter `git describe` would select
|
||||||
|
`nightly` and stamp binaries with it instead of `v<last release>-<n>-g<sha>`.
|
||||||
|
Keep the filter if you touch those scripts.
|
||||||
|
|
||||||
## Create a Tag
|
## Create a Tag
|
||||||
|
|
||||||
Use a semantic version tag:
|
Use a semantic version tag. Pushing it is what triggers `release.yml`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git status --short
|
git status --short
|
||||||
|
|
@ -12,8 +42,14 @@ git tag -a v0.2.0 -m "sshkeeper v0.2.0"
|
||||||
git push origin v0.2.0
|
git push origin v0.2.0
|
||||||
```
|
```
|
||||||
|
|
||||||
The release script uses `git describe --tags --always --dirty` by default. You
|
The remaining sections describe the manual equivalent, which is still the way
|
||||||
can also pass the version explicitly:
|
to test packaging locally or to recover if Actions is unavailable.
|
||||||
|
|
||||||
|
The release script uses `git describe --tags --match 'v*' --always --dirty` by
|
||||||
|
default. The `--match 'v*'` filter matters: nightly builds move a `nightly` tag
|
||||||
|
across `main`, and without the filter `git describe` would pick that tag and
|
||||||
|
stamp binaries `nightly` instead of `v<last release>-<n>-g<sha>`. You can also
|
||||||
|
pass the version explicitly:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./release.sh v0.2.0
|
./release.sh v0.2.0
|
||||||
|
|
@ -89,7 +125,7 @@ Expected result: every archive reports `OK`.
|
||||||
|
|
||||||
## Publish in GitHub Release
|
## Publish in GitHub Release
|
||||||
|
|
||||||
Upload these files to the release:
|
`release.yml` does this automatically on tag push. To publish by hand, upload:
|
||||||
|
|
||||||
- all five platform archives
|
- all five platform archives
|
||||||
- `checksums.txt`
|
- `checksums.txt`
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ set -euo pipefail
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
APP=sshkeeper
|
APP=sshkeeper
|
||||||
VERSION=${VERSION:-${1:-$(git describe --tags --always --dirty 2>/dev/null || echo "dev")}}
|
# --match 'v*' ignores the rolling `nightly` tag; see build.sh for the details.
|
||||||
|
VERSION=${VERSION:-${1:-$(git describe --tags --match 'v*' --always --dirty 2>/dev/null || echo "dev")}}
|
||||||
LDFLAGS="-s -w -X main.version=${VERSION}"
|
LDFLAGS="-s -w -X main.version=${VERSION}"
|
||||||
DIST_DIR="dist"
|
DIST_DIR="dist"
|
||||||
SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH:-$(git log -1 --format=%ct 2>/dev/null || date +%s)}
|
SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH:-$(git log -1 --format=%ct 2>/dev/null || date +%s)}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue