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")"
|
||||
|
||||
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}"
|
||||
|
||||
echo "==> Building ${APP} ${VERSION}..."
|
||||
|
|
|
|||
|
|
@ -1,10 +1,40 @@
|
|||
# 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
|
||||
|
||||
Use a semantic version tag:
|
||||
Use a semantic version tag. Pushing it is what triggers `release.yml`:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
|
|
@ -12,8 +42,14 @@ git tag -a v0.2.0 -m "sshkeeper v0.2.0"
|
|||
git push origin v0.2.0
|
||||
```
|
||||
|
||||
The release script uses `git describe --tags --always --dirty` by default. You
|
||||
can also pass the version explicitly:
|
||||
The remaining sections describe the manual equivalent, which is still the way
|
||||
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
|
||||
./release.sh v0.2.0
|
||||
|
|
@ -89,7 +125,7 @@ Expected result: every archive reports `OK`.
|
|||
|
||||
## 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
|
||||
- `checksums.txt`
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ set -euo pipefail
|
|||
cd "$(dirname "$0")"
|
||||
|
||||
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}"
|
||||
DIST_DIR="dist"
|
||||
SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH:-$(git log -1 --format=%ct 2>/dev/null || date +%s)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue