Repository navigation
v3.0.0 #61
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| # Both ways of cutting a release are supported, and both end with the Homebrew | |
| # tap bumped automatically: | |
| # | |
| # prereleased - the release is published as a pre-release. `build` uploads | |
| # every asset, `publish-release` then flips the release to official, and | |
| # `update-homebrew` bumps the tap. | |
| # released - the release is published directly as official (or an existing | |
| # pre-release is flipped by hand). `publish-release` is skipped because | |
| # there is nothing to convert, so `update-homebrew` keys off `build`. | |
| # | |
| # Before `released` was listed here, a release published directly as official | |
| # started no run at all: no binaries, no assets, no formula bump. Listing it | |
| # cannot loop back on us either, because `publish-release` performs its flip | |
| # with the default GITHUB_TOKEN and events raised by GITHUB_TOKEN do not start | |
| # new workflow runs. | |
| release: | |
| types: [prereleased, released] | |
| workflow_dispatch: | |
| inputs: | |
| update_homebrew: | |
| description: 'Update Homebrew formula after build' | |
| required: false | |
| default: 'false' | |
| type: choice | |
| options: ['true','false'] | |
| release_tag: | |
| description: 'Release tag to upload artifacts to (e.g. v1.2.3)' | |
| required: false | |
| permissions: | |
| contents: write | |
| jobs: | |
| build: | |
| name: Build ${{ matrix.target }} | |
| runs-on: ${{ matrix.os }} | |
| environment: packaging | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # Linux x86_64 (glibc) | |
| - target: x86_64-unknown-linux-gnu | |
| os: ubuntu-22.04 | |
| artifact_name: bssh | |
| asset_name: bssh-linux-x86_64 | |
| archive_ext: ".tar.gz" | |
| # Linux x86_64 (musl - static) | |
| - target: x86_64-unknown-linux-musl | |
| os: ubuntu-latest | |
| artifact_name: bssh | |
| asset_name: bssh-linux-x86_64-musl | |
| archive_ext: ".tar.gz" | |
| # Linux ARM64 (glibc) | |
| - target: aarch64-unknown-linux-gnu | |
| os: ubuntu-22.04-arm | |
| artifact_name: bssh | |
| asset_name: bssh-linux-aarch64 | |
| archive_ext: ".tar.gz" | |
| # Linux ARM64 (musl - static) | |
| - target: aarch64-unknown-linux-musl | |
| os: ubuntu-24.04-arm | |
| artifact_name: bssh | |
| asset_name: bssh-linux-aarch64-musl | |
| archive_ext: ".tar.gz" | |
| # macOS ARM64 | |
| - target: aarch64-apple-darwin | |
| os: macos-14 | |
| artifact_name: bssh | |
| asset_name: bssh-macos-aarch64 | |
| asset_suffix: macos-aarch64 | |
| archive_ext: ".zip" | |
| steps: | |
| # 1) Checkout repository | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| # 2) Cache Cargo build artifacts | |
| - name: Cache cargo | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: ${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }} | |
| ${{ runner.os }}-cargo-${{ matrix.target }}- | |
| # 3) Install Rust toolchain | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| # 4) Install musl tools only for aarch64 musl builds | |
| - name: Install musl tools (Linux musl only) | |
| if: contains(matrix.target, 'musl') | |
| run: | | |
| sudo apt update | |
| sudo apt install -y musl-tools | |
| # 5) Build release binaries (bssh, bssh-server, and bssh-keygen) | |
| - name: Build release binaries | |
| run: cargo build --release --target ${{ matrix.target }} --locked --bin bssh --bin bssh-server --bin bssh-keygen | |
| # 6) macOS code signing and notarization | |
| # | |
| # Gatekeeper accepts a downloaded binary only when BOTH hold: the code is | |
| # signed by a "Developer ID Application" authority, and Apple has issued a | |
| # notarization ticket for it. Releases up to v2.4.1 satisfied neither. | |
| # They were signed with an "Apple Distribution" certificate, which is an | |
| # App Store submission identity that carries no Developer ID leaf | |
| # extension, and they were never submitted to notarytool. When that | |
| # certificate was later revoked, macOS went from warning to actively | |
| # killing installed binaries on launch and deleting them as malware. | |
| # | |
| # The two composite actions below are mirrored from continuum-router | |
| # (itself mirrored from backend.ai-go), which takes its binaries through | |
| # the same procedure. macos-signing-setup rejects a certificate that is | |
| # not Developer ID Application before anything is signed, and | |
| # macos-sign-notarize-binary asserts the resulting authority and hardened | |
| # runtime flag before it submits, so a wrong certificate now fails the | |
| # release instead of shipping quietly. | |
| - name: Prepare signing certificate and tools | |
| if: runner.os == 'macOS' | |
| uses: ./.github/actions/macos-signing-setup | |
| with: | |
| certificate: ${{ secrets.APPLE_CERTIFICATE }} | |
| certificate-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| # Official binaries must never ship unsigned, so a missing or wrong | |
| # certificate is a hard failure. | |
| required: "true" | |
| # 7) Package binaries (separate packages for bssh, bssh-server, and bssh-keygen) | |
| # | |
| # macOS packaging happens inside the signing action, because each zip has | |
| # to be built from the signed copy and then handed to notarytool as a | |
| # single artifact. One call per released binary. | |
| # | |
| # The identifier is passed from BUNDLE_ID (variable preferred over | |
| # secret: it is not sensitive, any user can read it with `codesign -dv`, | |
| # and a secret would be masked to *** in the verification output). With | |
| # BUNDLE_ID=com.lablup.bssh the three binaries seal as com.lablup.bssh, | |
| # com.lablup.bssh-server, and com.lablup.bssh-keygen. | |
| - name: Sign, package, and notarize bssh (macOS) | |
| if: runner.os == 'macOS' | |
| uses: ./.github/actions/macos-sign-notarize-binary | |
| with: | |
| binary-path: target/${{ matrix.target }}/release/bssh | |
| zip-name: ${{ matrix.asset_name }}${{ matrix.archive_ext }} | |
| identifier: ${{ vars.BUNDLE_ID || secrets.BUNDLE_ID }} | |
| extra-files: | | |
| docs/man/bssh.1 | |
| api-key-id: ${{ secrets.AC_API_KEY_ID }} | |
| api-issuer-id: ${{ secrets.AC_API_ISSUER_ID }} | |
| api-private-key: ${{ secrets.AC_API_PRIVATE_KEY_P8 }} | |
| - name: Sign, package, and notarize bssh-server (macOS) | |
| if: runner.os == 'macOS' | |
| uses: ./.github/actions/macos-sign-notarize-binary | |
| with: | |
| binary-path: target/${{ matrix.target }}/release/bssh-server | |
| zip-name: bssh-server-${{ matrix.asset_suffix }}${{ matrix.archive_ext }} | |
| identifier: ${{ vars.BUNDLE_ID || secrets.BUNDLE_ID }}-server | |
| extra-files: | | |
| docs/man/bssh-server.8 | |
| api-key-id: ${{ secrets.AC_API_KEY_ID }} | |
| api-issuer-id: ${{ secrets.AC_API_ISSUER_ID }} | |
| api-private-key: ${{ secrets.AC_API_PRIVATE_KEY_P8 }} | |
| - name: Sign, package, and notarize bssh-keygen (macOS) | |
| if: runner.os == 'macOS' | |
| uses: ./.github/actions/macos-sign-notarize-binary | |
| with: | |
| binary-path: target/${{ matrix.target }}/release/bssh-keygen | |
| zip-name: bssh-keygen-${{ matrix.asset_suffix }}${{ matrix.archive_ext }} | |
| identifier: ${{ vars.BUNDLE_ID || secrets.BUNDLE_ID }}-keygen | |
| extra-files: | | |
| docs/man/bssh-keygen.1 | |
| api-key-id: ${{ secrets.AC_API_KEY_ID }} | |
| api-issuer-id: ${{ secrets.AC_API_ISSUER_ID }} | |
| api-private-key: ${{ secrets.AC_API_PRIVATE_KEY_P8 }} | |
| - name: Package Linux binaries (tar.gz) | |
| if: runner.os == 'Linux' | |
| run: | | |
| BIN_DIR=target/${{ matrix.target }}/release | |
| ASSET_BASE="${{ matrix.asset_name }}" | |
| SERVER_ASSET_BASE="${ASSET_BASE/bssh/bssh-server}" | |
| KEYGEN_ASSET_BASE="${ASSET_BASE/bssh/bssh-keygen}" | |
| # Package bssh | |
| mkdir -p package-bssh | |
| cp "$BIN_DIR/bssh" package-bssh/ | |
| cp docs/man/bssh.1 package-bssh/ | |
| tar -C package-bssh -czf "${ASSET_BASE}.tar.gz" . | |
| # Package bssh-server | |
| mkdir -p package-bssh-server | |
| cp "$BIN_DIR/bssh-server" package-bssh-server/ | |
| cp docs/man/bssh-server.8 package-bssh-server/ | |
| tar -C package-bssh-server -czf "${SERVER_ASSET_BASE}.tar.gz" . | |
| # Package bssh-keygen | |
| mkdir -p package-bssh-keygen | |
| cp "$BIN_DIR/bssh-keygen" package-bssh-keygen/ | |
| cp docs/man/bssh-keygen.1 package-bssh-keygen/ | |
| tar -C package-bssh-keygen -czf "${KEYGEN_ASSET_BASE}.tar.gz" . | |
| # 8) Generate checksums | |
| - name: Generate checksums | |
| run: | | |
| ASSET_BASE="${{ matrix.asset_name }}" | |
| SERVER_ASSET_BASE="${ASSET_BASE/bssh/bssh-server}" | |
| KEYGEN_ASSET_BASE="${ASSET_BASE/bssh/bssh-keygen}" | |
| EXT="${{ matrix.archive_ext }}" | |
| for file in "${ASSET_BASE}${EXT}" "${SERVER_ASSET_BASE}${EXT}" "${KEYGEN_ASSET_BASE}${EXT}"; do | |
| if [[ "$RUNNER_OS" == "Linux" ]]; then | |
| sha256sum "$file" > "$file.sha256" | |
| else | |
| shasum -a 256 "$file" > "$file.sha256" | |
| fi | |
| done | |
| # 9) Upload release artifacts and checksums | |
| - name: Upload release artifacts | |
| if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' | |
| run: | | |
| ASSET_BASE="${{ matrix.asset_name }}" | |
| SERVER_ASSET_BASE="${ASSET_BASE/bssh/bssh-server}" | |
| KEYGEN_ASSET_BASE="${ASSET_BASE/bssh/bssh-keygen}" | |
| EXT="${{ matrix.archive_ext }}" | |
| TAG="${{ github.event.release.tag_name || github.event.inputs.release_tag }}" | |
| gh release upload "$TAG" \ | |
| "${ASSET_BASE}${EXT}" \ | |
| "${ASSET_BASE}${EXT}.sha256" \ | |
| "${SERVER_ASSET_BASE}${EXT}" \ | |
| "${SERVER_ASSET_BASE}${EXT}.sha256" \ | |
| "${KEYGEN_ASSET_BASE}${EXT}" \ | |
| "${KEYGEN_ASSET_BASE}${EXT}.sha256" \ | |
| --clobber | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| # ============================================================================ | |
| # Publish pre-release as official release (after all builds complete) | |
| # ============================================================================ | |
| publish-release: | |
| name: Publish pre-release as official | |
| needs: [build] | |
| # A dispatch has to be able to finish a release too, not only build one. | |
| # Rebuilding a tag by hand previously stopped at "artifacts uploaded", so a | |
| # release rescued that way stayed a pre-release, and the Homebrew formula | |
| # job resolves through `releases/latest`, which never returns one. | |
| if: >- | |
| (github.event_name == 'release' && github.event.release.prerelease) || | |
| (github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag != '') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Convert pre-release to official release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ github.event.release.tag_name || github.event.inputs.release_tag }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "$TAG" ]; then | |
| echo "::error::no release tag to promote" | |
| exit 1 | |
| fi | |
| # Read the state into a variable rather than testing the command | |
| # substitution inline. A failing `gh` there yields an empty string, | |
| # `set -e` does not fire inside a test, and empty is not "true", so | |
| # the guard would conclude "already promoted" and exit 0 having done | |
| # nothing while reporting success. | |
| IS_PRERELEASE="$(gh release view "$TAG" --json isPrerelease -q .isPrerelease)" | |
| if [ -z "$IS_PRERELEASE" ]; then | |
| echo "::error::could not read the release state for $TAG; refusing to guess whether it needs promoting" | |
| exit 1 | |
| fi | |
| # Idempotent: re-running a dispatch against an already-promoted tag | |
| # is a normal thing to do while recovering, and must not fail. | |
| if [ "$IS_PRERELEASE" != "true" ]; then | |
| echo "$TAG is already a full release; nothing to promote" | |
| exit 0 | |
| fi | |
| # `--latest` is explicit rather than left to GitHub's own ordering, | |
| # so the tag Homebrew resolves is the one this run promoted. | |
| gh release edit "$TAG" --prerelease=false --latest | |
| # ============================================================================ | |
| # Update Homebrew formula (after every release asset has been uploaded) | |
| # ============================================================================ | |
| # | |
| # `build` is listed in `needs` explicitly rather than relied on transitively | |
| # through `publish-release`. The called workflow downloads the published | |
| # assets and hashes them, so it must never start before all five build legs | |
| # have finished uploading, and that guarantee has to hold on the path where | |
| # `publish-release` does not run at all. `publish-release` stays in `needs` so | |
| # the pre-release path is still ordered behind the flip to official. | |
| # | |
| # `!cancelled()` is required: `publish-release` is legitimately skipped when | |
| # the release was published as official already, and under the implicit | |
| # `success()` a job whose dependency was skipped is skipped too. A bare | |
| # `always()` would be the wrong way to lift that, since it would also fire | |
| # after a failed build and push a formula whose sha256 values point at assets | |
| # that were never uploaded, so the build result is asserted explicitly instead | |
| # of leaning on job ordering. | |
| # | |
| # On a manual `workflow_dispatch` of this workflow the formula is updated only | |
| # when the `update_homebrew` input asks for it, so rebuilding a tag by hand | |
| # does not push a tap commit as a side effect. | |
| update-homebrew: | |
| name: Update Homebrew formula | |
| needs: [build, publish-release] | |
| if: >- | |
| !cancelled() | |
| && needs.build.result == 'success' | |
| && (needs.publish-release.result == 'success' || needs.publish-release.result == 'skipped') | |
| && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.update_homebrew == 'true')) | |
| uses: ./.github/workflows/update_homebrew_formula.yml | |
| with: | |
| release_tag: ${{ github.event.release.tag_name || github.event.inputs.release_tag }} | |
| secrets: inherit | |
| # ============================================================================ | |
| # Microsoft Teams release notification (Power Automate Workflows webhook) | |
| # ============================================================================ | |
| notify-teams: | |
| name: Notify Teams on release | |
| needs: [build] | |
| if: github.event_name == 'release' | |
| runs-on: ubuntu-latest | |
| permissions: {} | |
| steps: | |
| - name: Build Adaptive Card payload | |
| env: | |
| TAG: ${{ github.event.release.tag_name }} | |
| NAME: ${{ github.event.release.name }} | |
| URL: ${{ github.event.release.html_url }} | |
| BODY: ${{ github.event.release.body }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| TRIMMED=$(printf '%s' "$BODY" | head -c 2000) | |
| jq -n \ | |
| --arg tag "$TAG" --arg name "$NAME" \ | |
| --arg url "$URL" --arg body "$TRIMMED" --arg repo "$REPO" ' | |
| { | |
| type: "message", | |
| attachments: [{ | |
| contentType: "application/vnd.microsoft.card.adaptive", | |
| content: { | |
| "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", | |
| type: "AdaptiveCard", | |
| version: "1.5", | |
| body: [ | |
| { type: "TextBlock", size: "Large", weight: "Bolder", | |
| text: ("🚀 " + $repo + " " + $tag + " released") }, | |
| { type: "TextBlock", text: $name, wrap: true, isSubtle: true }, | |
| { type: "TextBlock", text: $body, wrap: true } | |
| ], | |
| actions: [ | |
| { type: "Action.OpenUrl", title: "View release", url: $url } | |
| ] | |
| } | |
| }] | |
| }' > card.json | |
| - name: POST to Teams workflow | |
| if: env.WEBHOOK_URL != '' | |
| env: | |
| WEBHOOK_URL: ${{ secrets.TEAMS_RELEASE_NOTIFICATION_WORKFLOW_URL }} | |
| run: | | |
| curl -sSf -X POST \ | |
| -H "Content-Type: application/json" \ | |
| --data-binary @card.json \ | |
| "$WEBHOOK_URL" |