From dea2cd5fe1c886c783a981603e6007184c97c5ce Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Tue, 5 Dec 2023 15:24:50 -0500 Subject: [PATCH] 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. --- .github/workflows/hsm-testing.yml | 127 ++++++++++++++++++++++++++++++ .goreleaser-hsm.yml | 93 ++++++++++++++++++++++ script/release-hsm | 120 ++++++++++++++++++++++++++++ 3 files changed, 340 insertions(+) create mode 100644 .github/workflows/hsm-testing.yml create mode 100644 .goreleaser-hsm.yml create mode 100755 script/release-hsm diff --git a/.github/workflows/hsm-testing.yml b/.github/workflows/hsm-testing.yml new file mode 100644 index 000000000..31e31abab --- /dev/null +++ b/.github/workflows/hsm-testing.yml @@ -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 \ No newline at end of file diff --git a/.goreleaser-hsm.yml b/.goreleaser-hsm.yml new file mode 100644 index 000000000..7d3b975a5 --- /dev/null +++ b/.goreleaser-hsm.yml @@ -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: GitHub’s 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" diff --git a/script/release-hsm b/script/release-hsm new file mode 100755 index 000000000..661e10853 --- /dev/null +++ b/script/release-hsm @@ -0,0 +1,120 @@ +#!/bin/bash +set -e + +print_help() { + cat < [--platform {linux|macos|windows}] [--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