Skip to content

Commit

Permalink
feat: apple signing, static builds, easier template clones, better do…
Browse files Browse the repository at this point in the history
…cs (#44)

* feat: support quill (#43)

* docs: update README about Apple signing

* feat: improve docker for initial use template clone
  • Loading branch information
ekristen authored Feb 3, 2024
1 parent 8242ffa commit f650ad9
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 48 deletions.
26 changes: 21 additions & 5 deletions .github/workflows/release.yml → .github/workflows/goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,14 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: install cosign
uses: sigstore/cosign-installer@v3
- name: setup cosign
run: |
echo "${COSIGN_KEY}" > "$GITHUB_WORKSPACE/cosign.key"
- name: install quill
env:
COSIGN_KEY: ${{ secrets.COSIGN_KEY }}
QUILL_VERSION: 0.4.1
run: |
curl -Lo /tmp/quill_${QUILL_VERSION}_linux_amd64.tar.gz https://github.com/anchore/quill/releases/download/v${QUILL_VERSION}/quill_${QUILL_VERSION}_linux_amd64.tar.gz
tar -xvf /tmp/quill_${QUILL_VERSION}_linux_amd64.tar.gz -C /tmp
mv /tmp/quill /usr/local/bin/quill
chmod +x /usr/local/bin/quill
- name: set goreleaser default args
if: startsWith(github.ref, 'refs/tags/') == true
run: |
Expand All @@ -64,7 +67,20 @@ jobs:
- name: set goreleaser args renovate
if: startsWith(github.ref, 'refs/heads/renovate') == true
run: |
echo "GORELEASER_ARGS=--snapshot --skip-publish" >> $GITHUB_ENV
echo "GORELEASER_ARGS=--snapshot --skip publish --skip sign" >> $GITHUB_ENV
- name: setup-quill
uses: 1password/load-secrets-action@v1
# Extra Safeguard - This ensures the secrets are only loaded on tag and a tag that the repo owner triggered
if: startsWith(github.ref, 'refs/tags/') == true && github.actor == github.repository_owner
with:
export-env: true
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
QUILL_NOTARY_KEY: ${{ secrets.OP_QUILL_NOTARY_KEY }}
QUILL_NOTARY_KEY_ID: ${{ secrets.OP_QUILL_NOTARY_KEY_ID }}
QUILL_NOTARY_ISSUER: ${{ secrets.OP_QUILL_NOTARY_ISSUER }}
QUILL_SIGN_PASSWORD: ${{ secrets.OP_QUILL_SIGN_PASSWORD }}
QUILL_SIGN_P12: ${{ secrets.OP_QUILL_SIGN_P12 }}
- name: run goreleaser
uses: goreleaser/goreleaser-action@v5
with:
Expand Down
63 changes: 55 additions & 8 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,36 @@ release:
github:
owner: ekristen
name: go-project-template
prerelease: auto
env:
- REGISTRY=ghcr.io
- IMAGE=ekristen/go-project-template
builds:
- id: go-project-template
goos:
- linux
- darwin
- windows
- darwin
goarch:
- amd64
- arm64
ldflags:
- -s
- -w
- -extldflags="-static"
- -X '{{ .ModulePath }}/pkg/common.SUMMARY=v{{ .Version }}'
- -X '{{ .ModulePath }}/pkg/common.BRANCH={{ .Branch }}'
- -X '{{ .ModulePath }}/pkg/common.VERSION={{ .Tag }}'
- -X '{{ .ModulePath }}/pkg/common.COMMIT={{ .Commit }}'
hooks:
post:
- cmd: |
{{- if eq .Os "darwin" -}}
quill sign-and-notarize "{{ .Path }}" --dry-run={{ .IsSnapshot }} --ad-hoc={{ .IsSnapshot }} -vv
{{- else -}}
true
{{- end -}}
env:
- QUILL_LOG_FILE=/tmp/quill-{{ .Target }}.log
archives:
- id: go-project-template
builds:
Expand All @@ -27,42 +41,75 @@ archives:
- goos: windows
format: zip
dockers:
- id: go-project-template
- id: linux-amd64
ids:
- go-project-template
use: buildx
goos: linux
goarch: amd64
dockerfile: Dockerfile
image_templates:
- ghcr.io/ekristen/go-project-template:v{{ .Version }}
- ghcr.io/ekristen/go-project-template:{{ replace .Branch "/" "-" }}-{{ .ShortCommit }}-{{ .Timestamp }}
- '{{ .Env.REGISTRY }}/{{ .Env.IMAGE }}:v{{ .Version }}-amd64'
- '{{ .Env.REGISTRY }}/{{ .Env.IMAGE }}:{{ replace .Branch "/" "-" }}-{{ .ShortCommit }}-amd64-{{ .Timestamp }}'
build_flag_templates:
- "--platform=linux/amd64"
- "--target=goreleaser"
- "--pull"
- "--build-arg=PROJECT_NAME={{.ProjectName}}"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- '--label=org.opencontainers.image.source={{replace (replace (replace .GitURL "git@" "https://") ".git" "") "github.com:" "github.com/"}}'
- "--platform=linux/amd64"

- id: linux-arm64
ids:
- go-project-template
use: buildx
goos: linux
goarch: arm64
dockerfile: Dockerfile
image_templates:
- '{{ .Env.REGISTRY }}/{{ .Env.IMAGE }}:v{{ .Version }}-arm64'
- '{{ .Env.REGISTRY }}/{{ .Env.IMAGE }}:{{ replace .Branch "/" "-" }}-{{ .ShortCommit }}-arm64-{{ .Timestamp }}'
build_flag_templates:
- "--platform=linux/arm64"
- "--target=goreleaser"
- "--pull"
- "--build-arg=PROJECT_NAME={{.ProjectName}}"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- '--label=org.opencontainers.image.source={{replace (replace (replace .GitURL "git@" "https://") ".git" "") "github.com:" "github.com/"}}'
docker_manifests:
- name_template: '{{ .Env.REGISTRY }}/{{ .Env.IMAGE }}:v{{ .Version }}'
image_templates:
- '{{ .Env.REGISTRY }}/{{ .Env.IMAGE }}:v{{ .Version }}-amd64'
- '{{ .Env.REGISTRY }}/{{ .Env.IMAGE }}:v{{ .Version }}-arm64'
- name_template: '{{ .Env.REGISTRY }}/{{ .Env.IMAGE }}:{{ replace .Branch "/" "-" }}-{{ .ShortCommit }}-{{ .Timestamp }}'
image_templates:
- '{{ .Env.REGISTRY }}/{{ .Env.IMAGE }}:{{ replace .Branch "/" "-" }}-{{ .ShortCommit }}-amd64-{{ .Timestamp }}'
- '{{ .Env.REGISTRY }}/{{ .Env.IMAGE }}:{{ replace .Branch "/" "-" }}-{{ .ShortCommit }}-arm64-{{ .Timestamp }}'
signs:
- ids:
- go-project-template
- default
- darwin
cmd: cosign
signature: "${artifact}.sig"
certificate: "${artifact}.pem"
args: ["sign-blob", "--yes", "--oidc-provider=github", "--oidc-issuer=https://token.actions.githubusercontent.com", "--output-certificate=${certificate}", "--output-signature=${signature}", "${artifact}"]
artifacts: all
docker_signs:
- ids:
- go-project-template
- default
artifacts: all
cmd: cosign
args: ["sign", "--yes", "--oidc-provider=github", "--oidc-issuer=https://token.actions.githubusercontent.com", "--output-certificate=${certificate}", "--output-signature=${signature}", "${artifact}"]
checksum:
name_template: "checksums.txt"
snapshot:
name_template: '{{ trimprefix .Summary "v" }}'
# We are skipping changelog because we are using semantic release
changelog:
skip: true
17 changes: 10 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
# syntax=docker/dockerfile:1.6-labs

FROM debian:bullseye-slim as base
ARG PROJECT_NAME=go-project-template
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
RUN useradd -r -u 999 -d /home/go-project-template go-project-template
RUN useradd -r -u 999 -d /home/${PROJECT_NAME} ${PROJECT_NAME}

FROM ghcr.io/acorn-io/images-mirror/golang:1.21 AS build
ARG PROJECT_NAME=go-project-template
COPY / /src
WORKDIR /src
RUN \
--mount=type=cache,target=/go/pkg \
--mount=type=cache,target=/root/.cache/go-build \
go build -o bin/go-project-template main.go
go build -o bin/${PROJECT_NAME} main.go

FROM base AS goreleaser
COPY go-project-template /usr/local/bin/go-project-template
USER go-project-template
ARG PROJECT_NAME=go-project-template
COPY ${PROJECT_NAME} /usr/local/bin/${PROJECT_NAME}
USER ${PROJECT_NAME}

FROM base
COPY --from=build /src/bin/go-project-template /usr/local/bin/go-project-template
USER go-project-template
ARG PROJECT_NAME=go-project-template
COPY --from=build /src/bin/${PROJECT_NAME} /usr/local/bin/${PROJECT_NAME}
USER ${PROJECT_NAME}
58 changes: 54 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,51 @@ This is an opinionated go project template to use as a starting point for new pr
- Automated with GitHub Actions
- Builds with Docker
- While designed to use goreleaser, you can still just run `docker build`
- Apple Notary Signing Support
- Opinionated Layout
- Never use `internal/` folder
- Everything is under `pkg/` folder
- Automatic Dependency Management with [Renovate](https://github.com/renovatebot/renovate)
- Automatic Releases with [Release Drafter](https://github.com/release-drafter/release-drafter)
- Automatic [Semantic Releases](https://semantic-release.gitbook.io/)
- Documentation with Material for MkDocs
- API Server Example
- Uses Gorilla Mux (yes it's been archived, still the best option)
- Stubbed out Go Tests
- They are not comprehensive
- Stubbed out Go Tests (**note:** they are not comprehensive)

### Opinionated Decisions

- Uses `init` functions for registering commands globally.
- This allows for multiple `main` package files to be written and include different commands.
- Allows the command code to remain isolated from each other and a simple import to include the command.

### Multi-Platform Builds

This project is designed to build for multiple platforms, including macOS, Linux, and Windows. It also supports
multiple architectures including amd64 and arm64.

The goreleaser configuration is set up to build for all platforms and architectures by default. It even supports pushing
multi-architecture docker manifests by default. Some knowledge about GoReleaser's configuration is required should you
want to remove these capabilities.

### Apple Notary Signing

This makes use of a tool called [quill](https://github.com/anchore/quill). To make use of this feature you will need
to have an Apple Developer account and be able to create an Developer ID certificate.

The workflow is designed to pull the necessary secrets from 1Password. This is done to keep the secrets out of the
GitHub Actions logs. The secrets are pulled from 1Password if the event triggering the workflow is a tag **AND** the
actor is the owner of the repository. This is to prevent forks from being able to pull the secrets and is an extra
guard to help prevent theft.

GoReleaser is configured to always sign and notarize for macOS. However, it will not notarize if the build is a snapshot.

If configured properly, the binaries located within the archives produced by GoReleaser will be signed and notarized
by the Apple Notary Service and will automatically run on any macOS system without having to approve it under System
Preferences.

If you do not wish to use 1Password simply export the same environment variables using secrets to populate them. The
`QUILL_SIGN_P12` and `QUILL_NOTARY_KEY` need to be base64 encoded or paths to the actual files.

## Building

The following will build binaries in snapshot order.
Expand All @@ -38,14 +66,36 @@ goreleaser --clean --snapshot --skip sign

**Note:** we are skipping signing because this project uses cosign's keyless signing with GitHub Actions OIDC provider.

You can opt to generate a cosign keypair locally and set the following environment variables, and then you can run
`goreleaser --clean --snapshot` without the `--skip sign` flag to get signed artifacts.

Environment Variables:
-
- COSIGN_PASSWORD
- COSIGN_KEY (path to the key file) (recommend cosign.key, it is git ignored already)

```console
cosign generate-key-pair
```

## Configure

1. Rename Repository
2. Generate Cosign Keys
2. Generate Cosign Keys (optional if you want to run with signing locally, see above)
3. Update `.goreleaser.yml`, search/replace go-project-template with new project name, adjust GitHub owner
4. Update `main.go`,
5. Update `go.mod`, rename go project (using IDE is best so renames happen across all files)

### Docker

The Dockerfile is set up to build the project and then copy the artifacts from the build into the final image. It is
also configured to allow you to just run `docker build` directly if you do not want to use GoReleaser.

To make things easier and faster, the Dockerfile has a default build argument set to `go-project-template`. GoReleaser
will pass the new project name down (if you update the `.goreleaser.yml` file) and the Dockerfile will use that instead.

However, it would be better longer term to update this argument in the file or remove it all together.

### Signing

Signing happens via cosign's keyless features using the GitHub Actions OIDC provider.
Expand Down
71 changes: 47 additions & 24 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,50 +5,73 @@ This is an opinionated go project template to use as a starting point for new pr
## Features

- Builds with [GoReleaser](https://goreleaser.com)
- Automated with GitHub Actions
- Signed with Cosign (providing you generate a private key)
- Automated with GitHub Actions
- Signed with Cosign (providing you generate a private key)
- Linting with [golangci-lint](https://golangci-lint.run/)
- Automated with GitHub Actions
- Builds with Docker
- While designed to use goreleaser, you can still just run `docker build`
- While designed to use goreleaser, you can still just run `docker build`
- Apple Notary Signing Support
- Opinionated Layout
- Never use `internal/` folder
- Everything is under `pkg/` folder
- Never use `internal/` folder
- Everything is under `pkg/` folder
- Automatic Dependency Management with [Renovate](https://github.com/renovatebot/renovate)
- Automatic Releases with [Release Drafter](https://github.com/release-drafter/release-drafter)
- Automatic [Semantic Releases](https://semantic-release.gitbook.io/)
- Documentation with Material for MkDocs
- API Server Example
- Uses Gorilla Mux (yes it's been archived, still the best option)
- Stubbed out Go Tests
- They are not comprehensive
- Uses Gorilla Mux (yes it's been archived, still the best option)
- Stubbed out Go Tests (**note:** they are not comprehensive)

### Opinionated Decisions

- Uses `init` functions for registering commands globally.
- This allows for multiple `main` package files to be written and include different commands.
- Allows the command code to remain isolated from each other and a simple import to include the command.

## Building

The following will build binaries in snapshot order.

```console
goreleaser --clean --snapshot
goreleaser --clean --snapshot --skip sign
```

**Note:** we are skipping signing because this project uses cosign's keyless signing with GitHub Actions OIDC provider.

You can opt to generate a cosign keypair locally and set the following environment variables, and then you can run
`goreleaser --clean --snapshot` without the `--skip sign` flag to get signed artifacts.

Environment Variables:
-
- COSIGN_PASSWORD
- COSIGN_KEY (path to the key file) (recommend cosign.key, it is git ignored already)

```console
cosign generate-key-pair
```

## Configure

1. Rename Repository
2. Generate Cosign Keys
3. Update `.goreleaser.yml`
4. Update `main.go`
2. Generate Cosign Keys (optional if you want to run with signing locally, see above)
3. Update `.goreleaser.yml`, search/replace go-project-template with new project name, adjust GitHub owner
4. Update `main.go`,
5. Update `go.mod`, rename go project (using IDE is best so renames happen across all files)

### Signing

1. Create a password
- Recommend exporting in environment as `COSIGN_PASSWORD`
2. Generate cosign keys
3. Create GitHub Action Secrets
- `COSIGN_KEY` -> populate with cosign.key value
- `COSIGN_PASSWORD` -> populate with password from step 1
Signing happens via cosign's keyless features using the GitHub Actions OIDC provider.

#### Generate Keypair
### Releases

```console
cosign generate-key-pair
```
In order for Semantic Releases and GoReleaser to work properly you have to create a PAT to run Semantic Release
so it's actions against the repository can trigger other workflows. Unfortunately there is no way to trigger
a workflow from a workflow if both are run by the automatically generated GitHub Actions secret.

1. Create PAT that has content `write` permissions to the repository
2. Create GitHub Action Secret
- `SEMANTIC_GITHUB_TOKEN` -> populated with PAT from step 1
3. Done

## Documentation

Expand All @@ -69,4 +92,4 @@ OR (if you have docker)

```console
docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material
```
```

0 comments on commit f650ad9

Please sign in to comment.