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
68 changes: 0 additions & 68 deletions .github/workflows/docker-publish.yml

This file was deleted.

88 changes: 88 additions & 0 deletions .github/workflows/goreleaser.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: goreleaser

# Builds the release artifacts for an existing tag and pushes the container
# image. Called by release-pipeline.yml right after release-please cuts a tag,
# and available on its own to rebuild a tag whose image push failed.
on:
workflow_call:
inputs:
tag:
description: "Tag to build and publish the image for"
required: true
type: string
workflow_dispatch:
inputs:
tag:
description: "Existing tag to (re)build and publish the image for, e.g. v0.1.0"
required: true
type: string

permissions:
contents: write
packages: write

jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout tag
uses: actions/checkout@v7
with:
ref: ${{ inputs.tag }}
# GoReleaser derives the version from the tag, so a shallow checkout
# without tags would make it fall back to a snapshot version.
fetch-depth: 0

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

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Log in to ghcr.io
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v7
with:
distribution: goreleaser
version: "~> v2.18"
args: release --config .goreleaser.release.yml --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# release-please creates the release as a draft so it only becomes visible
# once the image it describes is actually pullable.
- name: Publish release
uses: actions/github-script@v9
env:
TAG: ${{ inputs.tag }}
with:
script: |
const { owner, repo } = context.repo;
const tag = process.env.TAG;

// getReleaseByTag does not return drafts, so list and match.
const releases = await github.paginate(
github.rest.repos.listReleases,
{ owner, repo, per_page: 100 }
);
const release = releases.find(r => r.tag_name === tag);
if (!release) {
throw new Error(`No release found for tag ${tag}`);
}

await github.rest.repos.updateRelease({
owner,
repo,
release_id: release.id,
draft: false,
});

console.log(`Published ${tag}: ${release.html_url}`);
55 changes: 55 additions & 0 deletions .github/workflows/release-pipeline.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: release-pipeline

# On every push to main, release-please keeps a release PR up to date from the
# conventional-commit history. Merging that PR is what cuts a release: it tags
# the commit, drafts the release notes, and hands the tag to goreleaser, which
# builds and pushes the versioned image before the release goes public.
on:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: write
pull-requests: write
packages: write

jobs:
release-please:
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
steps:
- name: release
id: release
uses: googleapis/release-please-action@v5
with:
# A PAT rather than GITHUB_TOKEN: pushes and PRs made with
# GITHUB_TOKEN do not trigger workflows, so the release PR would
# never run the tests it is meant to gate on.
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.json

# Nothing gets published without the tests passing. docker-publish.yml used to
# provide this gate; the release pipeline took over its job.
test:
needs: release-please
if: ${{ needs.release-please.outputs.release_created == 'true' }}
uses: ./.github/workflows/test.yml

# No `secrets: inherit`: the called workflow needs nothing beyond
# GITHUB_TOKEN, which reusable workflows always get, and inheriting would
# hand it RELEASE_PLEASE_TOKEN for no reason.
#
# If this job fails, the tag and the draft release already exist, so a later
# push to main will not retry it — release-please only reports
# release_created once. Re-run the goreleaser workflow directly instead; it
# takes the tag as a workflow_dispatch input for exactly this case.
goreleaser:
needs: [release-please, test]
if: ${{ needs.release-please.outputs.release_created == 'true' }}
uses: ./.github/workflows/goreleaser.yml
with:
tag: ${{ needs.release-please.outputs.tag_name }}
24 changes: 21 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
name: Test

# pull_request covers review; workflow_call lets docker-publish gate on this
# same job instead of duplicating it. Deliberately no push trigger: it would
# double-run on main, once here and once through the call.
# pull_request covers review; workflow_call lets the release pipeline gate on
# this same job instead of duplicating it. Deliberately no push trigger: it
# would double-run on main, once here and once through the call.
on:
pull_request:
workflow_call:
Expand All @@ -17,14 +17,32 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v7

# setup-go caches the module and build caches by default, keyed on go.sum.
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version-file: go.mod

- name: Check formatting (gofmt)
run: |
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
echo "Files not gofmt-formatted:"
echo "$unformatted"
exit 1
fi

- name: Vet
run: go vet ./...

# staticcheck itself needs a newer Go than the one go.mod pins for
# doormouse, so let the go command fetch that toolchain for this step.
# Tests still run on the version go.mod declares.
- name: Run staticcheck
run: go run honnef.co/go/tools/cmd/staticcheck@v0.8.1 ./...
env:
GOTOOLCHAIN: auto

# -race catches the data races the ManualClock tests are built to expose;
# -shuffle=on stops tests depending on declaration order.
- name: Test
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@
doormouse
go-wol-proxy
*.migrated.toml

# goreleaser output
dist/
60 changes: 60 additions & 0 deletions .goreleaser.release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
version: 2

project_name: doormouse

before:
hooks:
- go mod download

builds:
- id: doormouse
main: .
binary: doormouse
env:
- CGO_ENABLED=0
flags:
- -trimpath
ldflags:
- -s -w
# doormouse is a Linux daemon that has to sit in the target's broadcast
# domain, and the container image is the only release channel, so there is
# nothing to gain from darwin/windows builds.
goos: [linux]
goarch: [amd64, arm64]

# The container image is the only published artifact, so goreleaser neither
# builds archives nor touches the GitHub release. release-please owns the
# release and its notes; the workflow undrafts it once the image is pushed.
archives:
- formats: [binary]

release:
disable: true

changelog:
disable: true

dockers_v2:
- id: image
dockerfile: Dockerfile.release
ids: [doormouse]
images:
- ghcr.io/darksworm/doormouse
# Rolling tags let a compose file track a major or minor line and still get
# patch updates. :latest stays for the quick start in the README.
tags:
- "{{ .Version }}"
- "{{ .Major }}.{{ .Minor }}"
- "{{ .Major }}"
- latest
platforms:
- linux/amd64
- linux/arm64
labels:
org.opencontainers.image.created: "{{ .Date }}"
org.opencontainers.image.title: "{{ .ProjectName }}"
org.opencontainers.image.description: "A reverse proxy that wakes your servers when someone knocks"
org.opencontainers.image.revision: "{{ .FullCommit }}"
org.opencontainers.image.version: "{{ .Version }}"
org.opencontainers.image.licenses: "GPL-3.0-or-later"
org.opencontainers.image.source: "https://github.com/darksworm/doormouse"
3 changes: 3 additions & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
".": "0.0.0"
}
18 changes: 18 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@ Common types are:
- `docs` for documentation changes
- `chore` for maintenance

Commit types decide the next version number, so pick them with care.
`feat` bumps the minor version and `fix` the patch version; `docs` and `chore`
do not trigger a release. Mark a breaking change with a `!` after the type, as
in `feat!: drop the old target syntax`.

## Releases
Releases are automatic. [release-please](https://github.com/googleapis/release-please)
reads the commits landing on `main` and keeps a release pull request open with
the next version and a generated changelog. Merging that pull request tags the
release and publishes the container image; nothing else needs doing by hand.

The pipeline needs one secret, `RELEASE_PLEASE_TOKEN`, a personal access token
with write access to contents, pull requests and issues. When it expires,
release pull requests simply stop appearing. Run
`scripts/rotate-release-token.sh` to set it up or replace it: it opens the form,
says what to tick, checks the token can actually write to the repository before
storing it, and prints the date it expires.

## Pull requests
- Rebase on the latest `main` branch before submitting.
- Keep commits focused; separate unrelated changes.
Expand Down
18 changes: 18 additions & 0 deletions Dockerfile.release
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Runtime image for released versions. Unlike the top-level Dockerfile, which
# compiles from source for local builds, this one only packages the binary
# GoReleaser has already cross-compiled — so there is no RUN step and no
# emulation cost when building the arm64 image on an amd64 runner.
FROM alpine:3.22
Comment thread
darksworm marked this conversation as resolved.

WORKDIR /app

# GoReleaser dockers_v2 places each platform's binary under $TARGETPLATFORM/
ARG TARGETPLATFORM
COPY ${TARGETPLATFORM}/doormouse /app/doormouse

# Same contract as the source-built image: the default port, and a config
# mounted at /app/config.toml. TCP routes listen on their own ports; with
# network_mode: host they are reachable directly, otherwise publish each one.
EXPOSE 8080

ENTRYPOINT ["/app/doormouse", "/app/config.toml"]
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,22 @@ or checked into a config repo.

A config may use one format or the other, never both.

## Container images

Every release publishes an image to `ghcr.io/darksworm/doormouse`, built for
`linux/amd64` and `linux/arm64`. Four tags point at it:

| Tag | Points at |
| --- | --- |
| `0.4.1` | that exact release, and never moves |
| `0.4` | the newest patch in the 0.4 line |
| `0` | the newest release in the 0.x line |
| `latest` | the newest release |

`latest` is fine for trying doormouse out. Once it is proxying something you
care about, pin the exact version or the minor line, so an upgrade happens when
you choose it.

## Building from source

```bash
Expand Down
Loading