Skip to content

ci: drop --keychain-password from notarytool submit too #37

ci: drop --keychain-password from notarytool submit too

ci: drop --keychain-password from notarytool submit too #37

Workflow file for this run

name: CI
on:
push:
branches: [main, "feature/*", "fix/*"]
tags: ["v*"]
pull_request:
branches: [main]
workflow_dispatch:
# Cancel in-progress runs for the same branch / PR
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
MIX_ENV: test
OTP_VERSION: "29.0"
ELIXIR_VERSION: "1.20.1"
jobs:
# ── Linux: full CI quality gate ─────────────────────────────────────
linux:
name: "Linux · Elixir 1.20.1 · OTP 29.0"
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
otp-version: ${{ env.OTP_VERSION }}
elixir-version: ${{ env.ELIXIR_VERSION }}
- name: Cache build artifacts
uses: actions/cache@v4
with:
path: |
_build
deps
priv/plts
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
restore-keys: |
${{ runner.os }}-mix-
${{ runner.os }}-
- name: Install dependencies
run: mix deps.get
- name: Compile (warnings-as-errors)
# We intentionally don't pass --all-warnings: that flag treats
# third-party dep warnings (hpax bitstring ops, yamerl's
# deprecated catch, etc.) as errors. We only want to enforce
# warnings-as-errors on our own code, which `mix compile
# --warnings-as-errors` already does.
run: mix compile --warnings-as-errors
- name: Check formatting
run: mix format --check-formatted
- name: Credo (strict)
run: mix credo --strict
- name: Audit dependencies for CVEs
run: mix deps.audit
- name: Check for unused dependencies
run: mix deps.unlock --check-unused
- name: xref (no orphan modules)
run: mix xref graph --label compile-connected --fail-above 0
- name: Dialyzer
run: mix ci.dialyzer
timeout-minutes: 15
- name: Run tests with coverage
# mix test --cover runs the tests once and produces both
# the Elixir coverage report (cover/*.html) and the
# excoveralls JSON (cover/excoveralls.json via tool: ExCoveralls).
# The strict threshold check is disabled (threshold: 0 in
# mix.exs) until CLI integration tests are added.
run: mix test --cover
- name: Post coverage to Codecov
# Push the JSON to codecov.io using their bash uploader. This
# is the path recommended by excoveralls for Codecov.
#
# Requires CODECOV_TOKEN in repo Settings -> Secrets -> Actions.
# Get the token at https://codecov.io/gh/gilbertwong96/ado_cli
# -> Settings -> Upload Token. The bash uploader reads it from
# the CODECOV_TOKEN env var.
#
# Skip the step if the secret is not set. We don't fail the
# build over missing coverage uploads — coverage is still
# available as an action artifact download.
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
run: |
if [ -z "$CODECOV_TOKEN" ]; then
echo "::notice::CODECOV_TOKEN not set — skipping Codecov upload."
echo "Add it at https://codecov.io/gh/gilbertwong96/ado_cli"
echo "to enable the coverage badge."
exit 0
fi
curl -Os https://uploader.codecov.io/latest/linux/codecov
chmod +x codecov
./codecov --token "$CODECOV_TOKEN" --file ./cover/excoveralls.json
continue-on-error: true
- name: Upload coverage artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-linux
path: cover/excoveralls.json
retention-days: 30
# ── macOS: smoke test (the CLI is cross-compiled via Burrito) ──────
macos:
name: "macOS · Elixir 1.20.1"
runs-on: macos-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
otp-version: ${{ env.OTP_VERSION }}
elixir-version: ${{ env.ELIXIR_VERSION }}
- name: Cache build artifacts
uses: actions/cache@v4
with:
path: |
_build
deps
priv/plts
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
restore-keys: |
${{ runner.os }}-mix-
${{ runner.os }}-
- name: Install dependencies
run: mix deps.get
- name: Build escript
run: mix escript.build
- name: "Smoke test: --help works"
run: "./ado --help"
- name: "Smoke test: whoami with no auth returns friendly error"
run: |
rm -f "$HOME/.ado_cli/config.json"
./ado whoami || true
- name: "Smoke test: logout is idempotent"
run: ./ado logout
- name: Run tests
run: mix test
# ── Summary job — gate merge on all green ───────────────────────────
ci-status:
name: CI Status
if: always()
needs: [linux, macos]
runs-on: ubuntu-latest
steps:
- name: Fail if any matrix job failed
if: needs.linux.result != 'success' || needs.macos.result != 'success'
run: |
echo "Linux result: ${{ needs.linux.result }}"
echo "macOS result: ${{ needs.macos.result }}"
exit 1
# ── Release build: cross-compile via Burrito, rename, upload artifacts ─
release:
name: "Release · Burrito · ${{ matrix.label }}"
# Only build releases on pushes to main (not on PRs) and on tag pushes.
# Use workflow_dispatch to trigger manually for ad-hoc builds.
if: |
github.event_name == 'push' &&
(github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
needs: [ci-status]
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
# Don't cancel other platforms if one fails — we want all
# binaries even if e.g. macOS signing fails.
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: linux
cpu: x86_64
ext: ""
label: "Linux x86_64"
artifact: ado-linux-x86_64
sign: "false"
- os: ubuntu-24.04-arm
target: linux_arm
cpu: aarch64
ext: ""
label: "Linux ARM64"
artifact: ado-linux-aarch64
sign: "false"
- os: macos-latest
target: macos
cpu: aarch64
ext: ""
label: "macOS Apple Silicon"
artifact: ado-macos-aarch64
sign: "true"
- os: macos-latest
target: macos_x86
cpu: x86_64
ext: ""
label: "macOS Intel"
artifact: ado-macos-x86_64
sign: "true"
steps:
- uses: actions/checkout@v4
- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
otp-version: ${{ env.OTP_VERSION }}
elixir-version: ${{ env.ELIXIR_VERSION }}
env:
# On ubuntu-24.04-arm, ImageOS is reported as
# 'ubuntu24-arm64' which erlef/setup-beam doesn't recognize.
# Override to 'ubuntu24' (Erlang/Elixir prebuilt binaries
# are architecture-independent, so the ubuntu24 ones work
# fine on the arm64 runner).
ImageOS: ubuntu24
- name: Install Zig
# Burrito needs a specific zig version (0.16.0) to cross-compile
# BEAM. Install it via mlugg/setup-zig (the recommended
# successor to goto-bus-stop/setup-zig, which is now
# deprecated and hits GitHub's cache service unreliability).
uses: mlugg/setup-zig@v2
with:
version: 0.16.0
- name: Install xz
# Burrito needs xz to extract zig's tarball and musl. xz-utils
# ships with the Linux build image but not always; install
# defensively.
if: runner.os == 'Linux'
run: sudo apt-get -y install xz-utils
- name: Install xz (macOS)
# xz is rarely preinstalled on macOS runners.
if: runner.os == 'macOS'
run: brew install xz
- name: Verify zig/xz
run: |
zig version
xz --version
- name: Cache build artifacts
uses: actions/cache@v4
with:
path: |
_build
deps
priv/plts
~/.cache/burrito_file_cache
key: release-${{ runner.os }}-${{ hashFiles('**/mix.lock') }}
restore-keys: |
release-${{ runner.os }}-
- name: Install dependencies
run: mix deps.get
- name: Build Burrito release
env:
MIX_ENV: prod
# Burrito accepts BURRITO_TARGET=<target-key> to build only
# that target from the targets list in mix.exs. This avoids
# cross-compiling for all 4 platforms on each runner.
BURRITO_TARGET: ${{ matrix.target }}
# Burrito writes the binary to burrito_out/ado_<target> by
# convention. We rename the output below to a stable,
# versioned name.
run: |
mkdir -p release_bin
mix release --overwrite
ls -lh burrito_out/
- name: Get version
id: version
run: |
# Read the version from mix.exs so the release artifact name
# always matches the source of truth.
VERSION=$(grep -E '^\s*version:\s*"' mix.exs | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Rename binary
# Normalize the binary name to:
# ado-<version>-<os>-<arch><.exe?>
# e.g. ado-0.1.0-macos-aarch64
# ado-0.1.0-linux-x86_64
run: |
set -euo pipefail
VERSION="${{ steps.version.outputs.version }}"
# Map matrix.target to a clean {os}-{arch} string
case "${{ matrix.target }}" in
linux) SUFFIX="linux-x86_64" ;;
linux_arm) SUFFIX="linux-aarch64" ;;
macos) SUFFIX="macos-aarch64" ;;
macos_x86) SUFFIX="macos-x86_64" ;;
*) echo "::error::Unknown target ${{ matrix.target }}"; exit 1 ;;
esac
OUT_NAME="ado-${VERSION}-${SUFFIX}${{ matrix.ext }}"
SRC="burrito_out/ado_${{ matrix.target }}${{ matrix.ext }}"
DEST="release_bin/${OUT_NAME}"
if [[ ! -f "${SRC}" ]]; then
echo "::error::Expected Burrito output not found: ${SRC}"
ls -lh burrito_out/
exit 1
fi
mv "${SRC}" "${DEST}"
chmod +x "${DEST}"
echo "Renamed ${SRC} -> ${DEST}"
ls -lh release_bin/
- name: Smoke test the binary
# All release jobs run on a native runner for the target
# platform (e.g. ubuntu-24.04-arm for the Linux ARM build),
# so the binary can always be executed here.
run: |
set -euo pipefail
# Glob is intentional here — there's exactly one file in
# the dir, but actionlint warns about unquoted globs.
BINARY=$(ls release_bin/ado-*)
echo "Testing $BINARY"
file "$BINARY"
"$BINARY" --version || "$BINARY" --help | head -20
# ── macOS code signing + notarization ─────────────────────────────
# Only runs for matrix entries with sign: "true" (i.e. the two
# macOS targets). Requires these GitHub Secrets:
# MACOS_SIGN_IDENTITY e.g. "Developer ID Application: Foo (TEAMID1234)"
# MACOS_CERT_P12_BASE64 base64 of the .p12 export of the cert
# MACOS_CERT_P12_PASSWORD the .p12's export password
# MACOS_KEYCHAIN_PROFILE name of a notarytool keychain profile
# (set up locally with
# `xcrun notarytool store-credentials`)
# If any of these are missing on a non-tag push, the step
# skips with a warning so PR builds still succeed.
- name: Import code-signing certificate
if: matrix.sign == 'true'
env:
MACOS_CERT_P12_BASE64: ${{ secrets.MACOS_CERT_P12_BASE64 }}
MACOS_CERT_P12_PASSWORD: ${{ secrets.MACOS_CERT_P12_PASSWORD }}
run: |
set -euo pipefail
if [[ -z "${MACOS_CERT_P12_BASE64}" || -z "${MACOS_CERT_P12_PASSWORD}" ]]; then
echo "::warning::MACOS_CERT_P12_BASE64 / MACOS_CERT_P12_PASSWORD secrets not set; skipping signing."
echo "::warning::See SIGNING.md for the one-time setup."
exit 0
fi
KEYCHAIN_PATH="$RUNNER_TEMP/ado-signing.keychain-db"
KEYCHAIN_PASSWORD="$(openssl rand -hex 24)"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
# shellcheck disable=SC2046
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')
echo "$MACOS_CERT_P12_BASE64" | base64 --decode > "$RUNNER_TEMP/cert.p12"
security import "$RUNNER_TEMP/cert.p12" \
-k "$KEYCHAIN_PATH" \
-P "$MACOS_CERT_P12_PASSWORD" \
-T /usr/bin/codesign \
-T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
echo "MACOS_KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
echo "MACOS_KEYCHAIN_PASSWORD=$KEYCHAIN_PASSWORD" >> "$GITHUB_ENV"
- name: Sign macOS binary
if: matrix.sign == 'true'
env:
MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }}
run: |
set -euo pipefail
if [[ -z "${MACOS_SIGN_IDENTITY}" ]]; then
echo "::warning::MACOS_SIGN_IDENTITY secret not set; skipping signing."
exit 0
fi
if [[ ! -f "$MACOS_KEYCHAIN_PATH" ]]; then
echo "::warning::No keychain (secrets not set); skipping signing."
exit 0
fi
BINARY=$(ls release_bin/ado-*)
echo "Signing $BINARY with: $MACOS_SIGN_IDENTITY"
codesign \
--force \
--options runtime \
--timestamp \
--entitlements bin/ado.entitlements \
--sign "$MACOS_SIGN_IDENTITY" \
--keychain "$MACOS_KEYCHAIN_PATH" \
"$BINARY"
echo "Verifying signature"
codesign --verify --verbose=2 "$BINARY"
codesign -d --entitlements - "$BINARY" 2>&1 | head -20
- name: Notarize macOS binary
if: matrix.sign == 'true'
env:
# Profile name + the underlying Apple ID + app-specific password
# needed to recreate the profile in the temp keychain.
# (The profile itself only lives in the developer's local
# keychain; the CI runner has its own fresh keychain.)
MACOS_KEYCHAIN_PROFILE: ${{ secrets.MACOS_KEYCHAIN_PROFILE }}
MACOS_NOTARY_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }}
MACOS_NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID || '87R27UKGSF' }}
MACOS_NOTARY_PASSWORD: ${{ secrets.MACOS_NOTARY_PASSWORD }}
run: |
set -euo pipefail
BINARY=$(ls release_bin/ado-*)
if [[ -z "${MACOS_KEYCHAIN_PROFILE}" || -z "${MACOS_NOTARY_APPLE_ID}" || -z "${MACOS_NOTARY_PASSWORD}" ]]; then
echo "::warning::MACOS_KEYCHAIN_PROFILE / MACOS_NOTARY_APPLE_ID / MACOS_NOTARY_PASSWORD secrets not set; skipping notarization."
echo "::warning::Signed binary is OK for local use but Gatekeeper will warn on first run."
exit 0
fi
if [[ ! -f "$MACOS_KEYCHAIN_PATH" ]]; then
echo "::warning::No keychain (cert secrets not set); skipping notarization."
exit 0
fi
# notarytool store-credentials doesn't accept --keychain-password;
# it requires the keychain to already be unlocked via
# `security unlock-keychain` (which the import step already did).
echo "Recreating notarytool profile '$MACOS_KEYCHAIN_PROFILE' in temp keychain"
xcrun notarytool store-credentials "$MACOS_KEYCHAIN_PROFILE" \
--apple-id "$MACOS_NOTARY_APPLE_ID" \
--team-id "$MACOS_NOTARY_TEAM_ID" \
--password "$MACOS_NOTARY_PASSWORD" \
--keychain "$MACOS_KEYCHAIN_PATH"
# The submit command in this version of notarytool doesn't
# accept --keychain-password either. The keychain stays
# unlocked for 6 hours (set by set-keychain-settings -lut 21600
# in the import step), so it's still accessible here.
echo "Submitting for notarization (profile: $MACOS_KEYCHAIN_PROFILE)"
xcrun notarytool submit "$BINARY" \
--keychain-profile "$MACOS_KEYCHAIN_PROFILE" \
--keychain "$MACOS_KEYCHAIN_PATH" \
--wait
echo "Stapling notarization ticket"
xcrun stapler staple "$BINARY"
xcrun stapler validate "$BINARY"
echo "spctl assessment:"
spctl --assess --verbose=2 "$BINARY"
- name: Clean up keychain
if: always() && matrix.sign == 'true' && env.MACOS_KEYCHAIN_PATH != ''
run: |
if [[ -n "${MACOS_KEYCHAIN_PATH:-}" && -f "$MACOS_KEYCHAIN_PATH" ]]; then
security delete-keychain "$MACOS_KEYCHAIN_PATH" || true
# shellcheck disable=SC2046
security list-keychains -d user -s $(security list-keychains -d user | grep -v ado-signing | tr -d '"') || true
fi
- name: Upload release binary
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: release_bin/ado-*
retention-days: 90
if-no-files-found: error
# ── Attach binaries to GitHub Release on tag push ──────────────────
release-attach:
name: Publish GitHub Release
if: startsWith(github.ref, 'refs/tags/')
needs: [release]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
path: dist/
merge-multiple: true
- name: Display downloaded artifacts
run: ls -lh dist/
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: "AdoCli ${{ github.ref_name }}"
body: |
## AdoCli ${{ github.ref_name }}
Pre-built binaries for all supported platforms.
| Platform | Architecture | Binary |
|----------|--------------|--------|
| Linux | x86_64 | `ado-*-linux-x86_64` |
| Linux | aarch64 | `ado-*-linux-aarch64` |
| macOS | Apple Silicon | `ado-*-macos-aarch64` |
| macOS | Intel | `ado-*-macos-x86_64` |
See [README.md](https://github.com/gilbertwong96/ado_cli) for
installation instructions.
draft: false
prerelease: false
files: |
dist/ado-*