diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e57157e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +[*.yml] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/.github/utils/entitlements.plist b/.github/utils/entitlements.plist new file mode 100644 index 0000000..59cf377 --- /dev/null +++ b/.github/utils/entitlements.plist @@ -0,0 +1,9 @@ + + + + com.apple.security.network.server + + com.apple.security.network.client + + + \ No newline at end of file diff --git a/.github/utils/mac-sign-and-notarize.sh b/.github/utils/mac-sign-and-notarize.sh new file mode 100644 index 0000000..8f9c0d3 --- /dev/null +++ b/.github/utils/mac-sign-and-notarize.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# +# This script signs and notarizes a binary on macOS. +# It performs the following steps: +# 1. Creates a temporary keychain and imports the signing certificate from a base64-encoded .p12 file provided via environment variable. +# 2. Signs the app using the imported certificate. +# 3. Notarizes the app using Apple's notarytool. +# +# [1] - https://developer.apple.com/help/account/certificates/create-developer-id-certificates/ +# [2] - https://developer.apple.com/documentation/xcode/creating-distribution-signed-code-for-the-mac +# [3] - https://developer.apple.com/documentation/security/customizing-the-notarization-workflow#Upload-your-app-to-the-notarization-service + +set -eu + +# Script Configuration +APP_PATH=qbee-cli +ZIP_PATH=${1} +KEYCHAIN_TTL=300 # 5 minutes + +# Temporary working directory for keychain and certificate handling +WORKDIR="${TMPDIR:-/tmp}/mac-sign-and-notarize.$$" + +# Helper functions +die() { echo "Error: $*" >&2; exit 1; } +info(){ echo "==> $*"; } +cleanup(){ [ -d "$WORKDIR" ] && rm -rf "$WORKDIR"; } +trap cleanup EXIT INT TERM + +# Validate required environment variables +if [ -z "${MACOS_SIGN_P12:-}" ]; then + die "MACOS_SIGN_P12 must be set to the base64-encoded .p12 certificate" +fi + +if [ -z "${MACOS_SIGN_PASSWORD:-}" ]; then + die "MACOS_SIGN_PASSWORD must be set to the password for the .p12 certificate" +fi + +if [ -z "${APPLE_CODE_NOTARY_EMAIL:-}" ]; then + die "APPLE_CODE_NOTARY_EMAIL must be set to the Apple ID for notarization" +fi + +if [ -z "${APPLE_CODE_NOTARY_PASSWORD:-}" ]; then + die "APPLE_CODE_NOTARY_PASSWORD must be set to the password for the Apple ID for notarization" +fi + +if [ -z "${ZIP_PATH:-}" ]; then + die "$1 must be set to the path of the zip file (e.g., qbee-cli_darwin_amd64.zip)" +fi + +CERT_P12_PATH=${WORKDIR}/cert.p12 +KEYCHAIN_PATH=${WORKDIR}/keychain-db + +info "Generating random password for temporary keychain" +KEYCHAIN_PASSWORD=$(openssl rand -base64 32) + +info "Creating temporary working directory at $WORKDIR" +mkdir -p "$WORKDIR" || die "Cannot create temp dir" + +info "Decoding .p12 certificate and saving to $CERT_P12_PATH" +echo -n "$MACOS_SIGN_P12" | base64 --decode -o $CERT_P12_PATH + +info "Extracting signing identity from certificate" +MACOS_SIGN_IDENTITY=$( + openssl pkcs12 -legacy -in $CERT_P12_PATH -nokeys -passin env:MACOS_SIGN_PASSWORD -clcerts | \ + openssl x509 -noout -subject -nameopt sep_multiline,lname | \ + grep "commonName" | \ + cut -d= -f2) + +info $MACOS_SIGN_IDENTITY + +info "Extracting Apple Team ID from certificate" +APPLE_TEAM_ID=$( + openssl pkcs12 -legacy -in $CERT_P12_PATH -nokeys -passin env:MACOS_SIGN_PASSWORD -clcerts | \ + openssl x509 -noout -subject -nameopt sep_multiline,lname | \ + grep "organizationalUnitName" | \ + cut -d= -f2) + +info $APPLE_TEAM_ID + +info "Creating temporary keychain at $KEYCHAIN_PATH" +security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + +info "Setting keychain settings to prevent locking for $KEYCHAIN_TTL seconds" +security set-keychain-settings -lut $KEYCHAIN_TTL $KEYCHAIN_PATH + +info "Unlocking keychain" +security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + +info "Importing certificate into keychain" +security import $CERT_P12_PATH -k $KEYCHAIN_PATH -P "$MACOS_SIGN_PASSWORD" -T /usr/bin/codesign + +info "Allowing codesign to access the keychain item" +security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + +info "Setting keychain search path to include temporary keychain" +security list-keychains -d user -s $KEYCHAIN_PATH login.keychain + +info "Signing $APP_PATH with $MACOS_SIGN_IDENTITY certificate" +codesign --keychain $KEYCHAIN_PATH --force --options runtime --entitlements .github/utils/entitlements.plist --sign "$MACOS_SIGN_IDENTITY" --timestamp $APP_PATH + +info "Creating zip archive for notarization" +zip -r $ZIP_PATH $APP_PATH README.md LICENSE + +info "Storing notarization credentials in keychain for notarytool" +xcrun notarytool store-credentials "notarytool-password" \ + --keychain "$KEYCHAIN_PATH" \ + --apple-id "$APPLE_CODE_NOTARY_EMAIL" \ + --password "$APPLE_CODE_NOTARY_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" + +info "Notarizing binary with Apple Notary Service" +xcrun notarytool submit --keychain-profile "notarytool-password" --wait "$ZIP_PATH" diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 020f280..0a17a2f 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: 1.25.6 + go-version: 1.25.7 cache: false - name: golangci-lint uses: golangci/golangci-lint-action@v9 \ No newline at end of file diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index 5fb4205..f544052 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -15,7 +15,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: 1.25.6 + go-version: 1.25.7 - name: Build and test env: QBEE_EMAIL: ${{ secrets.QBEE_API_USER }} @@ -25,7 +25,7 @@ jobs: - id: govulncheck uses: golang/govulncheck-action@v1 with: - go-version-input: 1.25.6 + go-version-input: 1.25.7 go-package: ./... - name: golint run: go run golang.org/x/lint/golint@latest ./... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c8a1cb9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,164 @@ +name: Release qbee-cli on GitHub + +permissions: + contents: write # needed to write releases + id-token: write # needed for keyless signing + attestations: write # needed for provenance + +on: + release: + types: [prereleased] + +env: + # VERSION is set to the release tag name, e.g., "v1.0.0". + VERSION: ${{ github.ref_name }} + + # NAME is the name of the application. + NAME: qbee-cli + + # CGO_ENABLED disables CGO. + CGO_ENABLED: 0 + + # GCFLAGS with -trimpath ensures that the build is reproducible and does not contain file system paths. + GCFLAGS: -trimpath + + # LDFLAGS with -s and -w reduces the binary size by omitting symbol table and debug information. + # The -X flag sets the Version variable in the code to the release version. + DFLAGS: -s -w -X go.qbee.io/client.Version=${{ github.ref_name }} + + # GOPROXY ensures that we use the correct Go module proxy. + # We want our build process to start with populateding the Go proxy + # with the freshly tagged version of the module. + GOPROXY: https://proxy.golang.org + +jobs: + go-proxy: + name: Go Proxy + runs-on: ubuntu-24.04 + steps: + - name: Populate Go module proxy cache + run: go list -m go.qbee.io/client@${{ env.VERSION }} + + linux: + name: Linux Binaries + runs-on: ubuntu-24.04 + needs: go-proxy + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Install stable Go version + uses: actions/setup-go@v6 + with: + go-version: stable + + - name: Build binary + run: go build $GCFLAGS -ldflags "$LDFLAGS" -o ${{ env.NAME }} ./cmd + + - name: Create archive and upload to the release + id: reease + env: + GH_TOKEN: ${{ github.token }} + ARCHIVE_NAME: ${{ env.NAME }}_linux_amd64.tar.gz + run: | + tar -czvf $ARCHIVE_NAME ${{ env.NAME }} LICENSE README.md + gh release upload ${{ env.VERSION }} $ARCHIVE_NAME + + windows: + name: Windows Signed Binary + runs-on: windows-2025 + needs: go-proxy + env: + # The following environment variables are needed for the DigiCert Software Trust signing action. + # https://docs.digicert.com/en/digicert-keylocker/ci-cd-integrations-and-deployment-pipelines/plugins/github/binary-signing-using-github-actions.html + # Following credentials are from Qbee GitHub Actions service user account. + SM_HOST: ${{ vars.SM_HOST }} + SM_API_KEY: ${{ secrets.SM_API_KEY }} + SM_CLIENT_CERT_FILE: Certificate_pkcs12.p12 + SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Install stable Go version + uses: actions/setup-go@v6 + with: + go-version: stable + + - name: Setup DigiCert KeyLocker + shell: bash + run: | + echo "${{ secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > ${{ env.SM_CLIENT_CERT_FILE }} + + - name: Build binary + shell: bash + run: go build $GCFLAGS -ldflags "$LDFLAGS" -o ${{ env.NAME }}.exe ./cmd + + - name: Sign the binary + uses: digicert/code-signing-software-trust-action@v1 + with: + simple-signing-mode: true + keypair-alias: ${{ secrets.SM_KEYPAIR_ALIAS }} + input: ${{ env.NAME }}.exe + + - name: Create archive and upload to the release + shell: bash + env: + GH_TOKEN: ${{ github.token }} + ARCHIVE_NAME: ${{ env.NAME }}_windows_amd64.zip + run: | + 7z a $ARCHIVE_NAME ${{ env.NAME }}.exe LICENSE README.md + gh release upload ${{ env.VERSION }} $ARCHIVE_NAME + + macos: + name: MacOS Signed Binary + runs-on: macos-26 + needs: go-proxy + strategy: + matrix: + arch: [amd64, arm64] + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Install stable Go version + uses: actions/setup-go@v6 + with: + go-version: stable + + - name: Build binary for ${{ matrix.arch }} + env: + GOARCH: ${{ matrix.arch }} + run: go build $GCFLAGS -ldflags "$LDFLAGS" -o ${{ env.NAME }} ./cmd + + - name: Sign and package + env: + TMPDIR: ${{ runner.temp }} + + # MACOS_SIGN_P12 is the base64-encoded .p12 certificate file for Developer ID Application certificate type. + # The certificate can be created and downloaded from the Apple Developer account, Certificates section. + MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }} + + # MACOS_SIGN_PASSWORD is the password for the .p12 certificate file. + MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }} + + # APPLE_CODE_NOTARY_EMAIL is the Apple ID used for notarization. + # The user needs to have App Manager role. + APPLE_CODE_NOTARY_EMAIL: ${{ secrets.APPLE_CODE_NOTARY_EMAIL }} + + # APPLE_CODE_NOTARY_PASSWORD is the password for the Apple ID used for notarization + APPLE_CODE_NOTARY_PASSWORD: ${{ secrets.APPLE_CODE_NOTARY_PASSWORD }} + + run: bash ./.github/utils/mac-sign-and-notarize.sh ${{ env.NAME }}_darwin_${{ matrix.arch }}.zip + + - name: Upload Zip archive to the release + env: + GH_TOKEN: ${{ github.token }} + ARCHIVE_NAME: ${{ env.NAME }}_darwin_${{ matrix.arch }}.zip + run: gh release upload ${{ env.VERSION }} ${{ env.ARCHIVE_NAME }} diff --git a/go.mod b/go.mod index 7902e7c..628d096 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module go.qbee.io/client -go 1.25.6 +go 1.25.7 require ( github.com/xtaci/smux v1.5.55