Create HSM testing workflow

This commit is an initial prototype based on the deployment workflow, using the
Azure Code Signing service to sign Windows .exe and .msi files.

These changes have been isolated as much as possible to not affect existing
deployment workflows while also working around design issues with how GitHub
CLI workflow works with GoReleaser and now with ACS support.  The biggest smell
was over whether to break from using GoReleaser or have GoReleaser control as
much about the release process as it has been versus opening / signing /
archiving the resulting GoReleaser artifacts; needless to say, the latter was
chosen for expedience as well as leaning into officially supported solutions.
This commit is contained in:
Andy Feller 2023-12-05 15:24:50 -05:00
parent 3bb62d4724
commit dea2cd5fe1
3 changed files with 340 additions and 0 deletions

127
.github/workflows/hsm-testing.yml vendored Normal file
View file

@ -0,0 +1,127 @@
name: HSM Testing
run-name: ${{ inputs.tag_name }} / go ${{ inputs.go_version }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: true
permissions:
contents: write
on:
workflow_dispatch:
inputs:
tag_name:
required: true
type: string
go_version:
default: "1.21"
type: string
jobs:
windows:
runs-on: windows-latest
environment: production
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: ${{ inputs.go_version }}
- name: Install GoReleaser
uses: goreleaser/goreleaser-action@v5
with:
version: "~1.17.1"
install-only: true
- name: Build release binaries
shell: bash
env:
TAG_NAME: ${{ inputs.tag_name }}
run: script/release-hsm --local "$TAG_NAME" --platform windows --config .goreleaser-hsm.yml
# As official Azure HSM support for signing Windows .exe binaries is in the form of an action,
# we must unzip the archives created by GoReleaser, sign the binaries, and then re-zip them.
# This choice was due to the fact that GoReleaser produces
- name: Expand goreleaser archives for signing
shell: bash
run: |
for ZIP_FILE in dist/gh_*_windows_*.zip; do
unzip -d "${ZIP_FILE%.zip}" "$ZIP_FILE"
done
- name: Sign .exe release binaries
uses: azure/azure-code-signing-action@6c86237186b7eed50c9e8a3a6e42131bcc5e4601
with:
azure-tenant-id: ${{ secrets.SPN_SPN_AZURE_CODE_SIGNING_DEMO_TENANT_ID }}
azure-client-id: ${{ secrets.SPN_SPN_AZURE_CODE_SIGNING_DEMO_CLIENT_ID }}
azure-client-secret: ${{ secrets.SPN_SPN_AZURE_CODE_SIGNING_DEMO }}
endpoint: https://wus.codesigning.azure.net/
code-signing-account-name: GitHubInc
certificate-profile-name: GitHubInc
files-folder: ${{ github.workspace }}/dist
files-folder-filter: exe
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
- name: Zip goreleaser directories
shell: bash
run: |
for DIR in dist/gh_*_windows_*; do
zip -r "$DIR.zip" "$DIR"
done
- name: Set up MSBuild
id: setupmsbuild
uses: microsoft/setup-msbuild@v1.3.1
- name: Build MSI
shell: bash
env:
MSBUILD_PATH: ${{ steps.setupmsbuild.outputs.msbuildPath }}
run: |
for ZIP_FILE in dist/gh_*_windows_*.zip; do
MSI_NAME="$(basename "$ZIP_FILE" ".zip")"
MSI_VERSION="$(cut -d_ -f2 <<<"$MSI_NAME" | cut -d- -f1)"
case "$MSI_NAME" in
*_386 )
source_dir="$PWD/dist/windows_windows_386"
platform="x86"
;;
*_amd64 )
source_dir="$PWD/dist/windows_windows_amd64_v1"
platform="x64"
;;
*_arm64 )
echo "skipping building MSI for arm64 because WiX 3.11 doesn't support it: https://github.com/wixtoolset/issues/issues/6141" >&2
continue
#source_dir="$PWD/dist/windows_windows_arm64"
#platform="arm64"
;;
* )
printf "unsupported architecture: %s\n" "$MSI_NAME" >&2
exit 1
;;
esac
"${MSBUILD_PATH}\MSBuild.exe" ./build/windows/gh.wixproj -p:SourceDir="$source_dir" -p:OutputPath="$PWD/dist" -p:OutputName="$MSI_NAME" -p:ProductVersion="${MSI_VERSION#v}" -p:Platform="$platform"
done
- name: Sign .msi release binaries
uses: azure/azure-code-signing-action@6c86237186b7eed50c9e8a3a6e42131bcc5e4601
with:
azure-tenant-id: ${{ secrets.SPN_SPN_AZURE_CODE_SIGNING_DEMO_TENANT_ID }}
azure-client-id: ${{ secrets.SPN_SPN_AZURE_CODE_SIGNING_DEMO_CLIENT_ID }}
azure-client-secret: ${{ secrets.SPN_SPN_AZURE_CODE_SIGNING_DEMO }}
endpoint: https://wus.codesigning.azure.net/
code-signing-account-name: GitHubInc
certificate-profile-name: GitHubInc
files-folder: ${{ github.workspace }}/dist
files-folder-filter: msi
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
- uses: actions/upload-artifact@v3
with:
name: windows
if-no-files-found: error
retention-days: 7
path: |
dist/*.zip
dist/*.msi

93
.goreleaser-hsm.yml Normal file
View file

@ -0,0 +1,93 @@
project_name: gh
release:
prerelease: auto
draft: true # we only publish after the Windows MSI gets uploaded
name_template: "GitHub CLI {{.Version}}"
before:
hooks:
- >-
{{ if eq .Runtime.Goos "windows" }}echo{{ end }} make manpages GH_VERSION={{.Version}}
- >-
{{ if ne .Runtime.Goos "linux" }}echo{{ end }} make completions
builds:
- id: macos #build:macos
goos: [darwin]
goarch: [amd64, arm64]
hooks:
post:
- cmd: ./script/sign '{{ .Path }}'
output: true
binary: bin/gh
main: ./cmd/gh
ldflags:
- -s -w -X github.com/cli/cli/v2/internal/build.Version={{.Version}} -X github.com/cli/cli/v2/internal/build.Date={{time "2006-01-02"}}
- id: linux #build:linux
goos: [linux]
goarch: [386, arm, amd64, arm64]
env:
- CGO_ENABLED=0
binary: bin/gh
main: ./cmd/gh
ldflags:
- -s -w -X github.com/cli/cli/v2/internal/build.Version={{.Version}} -X github.com/cli/cli/v2/internal/build.Date={{time "2006-01-02"}}
- id: windows #build:windows
goos: [windows]
goarch: [386, amd64, arm64]
binary: bin/gh
main: ./cmd/gh
ldflags:
- -s -w -X github.com/cli/cli/v2/internal/build.Version={{.Version}} -X github.com/cli/cli/v2/internal/build.Date={{time "2006-01-02"}}
archives:
- id: linux-archive
builds: [linux]
name_template: "gh_{{ .Version }}_linux_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
wrap_in_directory: true
format: tar.gz
rlcp: true
files:
- LICENSE
- ./share/man/man1/gh*.1
- id: macos-archive
builds: [macos]
name_template: "gh_{{ .Version }}_macOS_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
wrap_in_directory: true
format: zip
rlcp: true
files:
- LICENSE
- ./share/man/man1/gh*.1
- id: windows-archive
builds: [windows]
name_template: "gh_{{ .Version }}_windows_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
wrap_in_directory: false
format: zip
rlcp: true
files:
- LICENSE
nfpms: #build:linux
- license: MIT
maintainer: GitHub
homepage: https://github.com/cli/cli
bindir: /usr
dependencies:
- git
description: GitHubs official command line tool.
formats:
- deb
- rpm
contents:
- src: "./share/man/man1/gh*.1"
dst: "/usr/share/man/man1"
- src: "./share/bash-completion/completions/gh"
dst: "/usr/share/bash-completion/completions/gh"
- src: "./share/fish/vendor_completions.d/gh.fish"
dst: "/usr/share/fish/vendor_completions.d/gh.fish"
- src: "./share/zsh/site-functions/_gh"
dst: "/usr/share/zsh/site-functions/_gh"

120
script/release-hsm Executable file
View file

@ -0,0 +1,120 @@
#!/bin/bash
set -e
print_help() {
cat <<EOF
To tag a new release:
script/release [--staging] <tag-name> [--platform {linux|macos|windows}] [--branch <branch>]
To build staging binaries from the current branch:
script/release --current [--platform {linux|macos|windows}]
To build binaries locally with goreleaser:
script/release --local --platform {linux|macos|windows}
EOF
}
if [ $# -eq 0 ]; then
print_help >&2
exit 1
fi
tag_name=""
is_local=""
do_push=""
platform=""
branch="trunk"
deploy_env="production"
goreleaser_config=".goreleaser.yml"
while [ $# -gt 0 ]; do
case "$1" in
-h | --help )
print_help
exit 0
;;
-b | --branch )
branch="$2"
shift 2
;;
-c | --config )
goreleaser_config="$2"
shift 2
;;
-p | --platform )
platform="$2"
shift 2
;;
--local )
is_local=1
shift 1
;;
--staging )
deploy_env="staging"
shift 1
;;
--current )
deploy_env="staging"
tag_name="$(git describe --tags --abbrev=0)"
branch="$(git rev-parse --symbolic-full-name '@{upstream}' 2>/dev/null || git branch --show-current)"
branch="${branch#refs/remotes/*/}"
do_push=1
shift 1
;;
-* )
printf "unrecognized flag: %s\n" "$1" >&2
exit 1
;;
* )
tag_name="$1"
shift 1
;;
esac
done
announce() {
local tmpdir="${TMPDIR:-/tmp}"
echo "$*" | sed "s:${tmpdir%/}:\$TMPDIR:"
"$@"
}
trigger_deployment() {
announce gh workflow -R cli/cli run deployment.yml --ref "$branch" -f tag_name="$tag_name" -f environment="$deploy_env"
}
build_local() {
local config="$goreleaser_config"
case "$platform" in
linux )
sed '/#build:windows/,/^$/d; /#build:macos/,/^$/d' .goreleaser.yml >.goreleaser.generated.yml
config=".goreleaser.generated.yml"
;;
macos )
sed '/#build:windows/,/^$/d; /#build:linux/,/^$/d' .goreleaser.yml >.goreleaser.generated.yml
config=".goreleaser.generated.yml"
;;
windows )
sed '/#build:linux/,/^$/d; /#build:macos/,/^$/d' .goreleaser.yml >.goreleaser.generated.yml
config=".goreleaser.generated.yml"
;;
esac
[ -z "$tag_name" ] || export GORELEASER_CURRENT_TAG="$tag_name"
announce goreleaser release -f "$config" --clean --skip-validate --skip-publish --release-notes="$(mktemp)"
}
if [ -n "$is_local" ]; then
build_local
else
if [ -n "$do_push" ]; then
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "refusing to continue due to uncomitted local changes" >&2
exit 1
fi
announce git push
fi
trigger_deployment
if [ "$deploy_env" = "production" ]; then
echo
echo "Go to Slack to manually approve this production deployment."
fi
fi