name: Create Release on: push: tags: - "*" release: types: [published] permissions: id-token: write contents: write jobs: set-version: name: Set version number if: github.event_name == 'push' runs-on: open-source-releaser outputs: version: ${{ steps.get_version.outputs.tag }} steps: - name: Set version number id: get_version run: | version="${{ github.ref_name }}" echo "tag=$version" >> $GITHUB_OUTPUT create-binaries: if: github.event_name == 'push' needs: set-version uses: ./.github/workflows/create-artifact.yml with: version: ${{ needs.set-version.outputs.version }} publish-binaries: name: Publish to GitHub release if: github.event_name == 'push' needs: [set-version, create-binaries] runs-on: open-source-releaser steps: - name: Checkout code uses: actions/checkout@v3 - name: Download all binary artifacts uses: actions/download-artifact@v4 with: path: binaries/ pattern: safe-chain-* merge-multiple: false - name: Rename binaries to include platform and architecture run: | mkdir release-artifacts mv binaries/safe-chain-macos-x64/safe-chain release-artifacts/safe-chain-macos-x64 mv binaries/safe-chain-macos-arm64/safe-chain release-artifacts/safe-chain-macos-arm64 mv binaries/safe-chain-linux-x64/safe-chain release-artifacts/safe-chain-linux-x64 mv binaries/safe-chain-linux-arm64/safe-chain release-artifacts/safe-chain-linux-arm64 mv binaries/safe-chain-linuxstatic-x64/safe-chain release-artifacts/safe-chain-linuxstatic-x64 mv binaries/safe-chain-linuxstatic-arm64/safe-chain release-artifacts/safe-chain-linuxstatic-arm64 mv binaries/safe-chain-win-x64/safe-chain.exe release-artifacts/safe-chain-win-x64.exe mv binaries/safe-chain-win-arm64/safe-chain.exe release-artifacts/safe-chain-win-arm64.exe - name: Move install scripts and hard-code version and checksums env: VERSION: ${{ needs.set-version.outputs.version }} run: | SHA_MACOS_X64=$(sha256sum release-artifacts/safe-chain-macos-x64 | awk '{print $1}') SHA_MACOS_ARM64=$(sha256sum release-artifacts/safe-chain-macos-arm64 | awk '{print $1}') SHA_LINUX_X64=$(sha256sum release-artifacts/safe-chain-linux-x64 | awk '{print $1}') SHA_LINUX_ARM64=$(sha256sum release-artifacts/safe-chain-linux-arm64 | awk '{print $1}') SHA_LINUXSTATIC_X64=$(sha256sum release-artifacts/safe-chain-linuxstatic-x64 | awk '{print $1}') SHA_LINUXSTATIC_ARM64=$(sha256sum release-artifacts/safe-chain-linuxstatic-arm64 | awk '{print $1}') SHA_WIN_X64=$(sha256sum release-artifacts/safe-chain-win-x64.exe | awk '{print $1}') SHA_WIN_ARM64=$(sha256sum release-artifacts/safe-chain-win-arm64.exe | awk '{print $1}') sed \ -e "s/\$(fetch_latest_version)/${VERSION}/" \ -e "s|^SHA256_MACOS_X64=\"\"|SHA256_MACOS_X64=\"${SHA_MACOS_X64}\"|" \ -e "s|^SHA256_MACOS_ARM64=\"\"|SHA256_MACOS_ARM64=\"${SHA_MACOS_ARM64}\"|" \ -e "s|^SHA256_LINUX_X64=\"\"|SHA256_LINUX_X64=\"${SHA_LINUX_X64}\"|" \ -e "s|^SHA256_LINUX_ARM64=\"\"|SHA256_LINUX_ARM64=\"${SHA_LINUX_ARM64}\"|" \ -e "s|^SHA256_LINUXSTATIC_X64=\"\"|SHA256_LINUXSTATIC_X64=\"${SHA_LINUXSTATIC_X64}\"|" \ -e "s|^SHA256_LINUXSTATIC_ARM64=\"\"|SHA256_LINUXSTATIC_ARM64=\"${SHA_LINUXSTATIC_ARM64}\"|" \ -e "s|^SHA256_WIN_X64=\"\"|SHA256_WIN_X64=\"${SHA_WIN_X64}\"|" \ -e "s|^SHA256_WIN_ARM64=\"\"|SHA256_WIN_ARM64=\"${SHA_WIN_ARM64}\"|" \ install-scripts/install-safe-chain.sh > release-artifacts/install-safe-chain.sh sed \ -e "s/\$Version = Get-LatestVersion/\$Version = \"${VERSION}\"/" \ -e "s|^\$SHA256_MACOS_X64 = \"\"|\$SHA256_MACOS_X64 = \"${SHA_MACOS_X64}\"|" \ -e "s|^\$SHA256_MACOS_ARM64 = \"\"|\$SHA256_MACOS_ARM64 = \"${SHA_MACOS_ARM64}\"|" \ -e "s|^\$SHA256_LINUX_X64 = \"\"|\$SHA256_LINUX_X64 = \"${SHA_LINUX_X64}\"|" \ -e "s|^\$SHA256_LINUX_ARM64 = \"\"|\$SHA256_LINUX_ARM64 = \"${SHA_LINUX_ARM64}\"|" \ -e "s|^\$SHA256_LINUXSTATIC_X64 = \"\"|\$SHA256_LINUXSTATIC_X64 = \"${SHA_LINUXSTATIC_X64}\"|" \ -e "s|^\$SHA256_LINUXSTATIC_ARM64 = \"\"|\$SHA256_LINUXSTATIC_ARM64 = \"${SHA_LINUXSTATIC_ARM64}\"|" \ -e "s|^\$SHA256_WIN_X64 = \"\"|\$SHA256_WIN_X64 = \"${SHA_WIN_X64}\"|" \ -e "s|^\$SHA256_WIN_ARM64 = \"\"|\$SHA256_WIN_ARM64 = \"${SHA_WIN_ARM64}\"|" \ install-scripts/install-safe-chain.ps1 > release-artifacts/install-safe-chain.ps1 cp install-scripts/uninstall-safe-chain.sh release-artifacts/uninstall-safe-chain.sh cp install-scripts/uninstall-safe-chain.ps1 release-artifacts/uninstall-safe-chain.ps1 cp install-scripts/install-endpoint-mac.sh release-artifacts/install-endpoint-mac.sh cp install-scripts/install-endpoint-windows.ps1 release-artifacts/install-endpoint-windows.ps1 cp install-scripts/uninstall-endpoint-mac.sh release-artifacts/uninstall-endpoint-mac.sh cp install-scripts/uninstall-endpoint-windows.ps1 release-artifacts/uninstall-endpoint-windows.ps1 - name: Create draft release and upload assets env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION: ${{ needs.set-version.outputs.version }} run: | if ! gh release view "$VERSION" &>/dev/null; then gh release create "$VERSION" --draft --title "$VERSION" --generate-notes fi gh release upload "$VERSION" --clobber \ release-artifacts/safe-chain-macos-x64 \ release-artifacts/safe-chain-macos-arm64 \ release-artifacts/safe-chain-linux-x64 \ release-artifacts/safe-chain-linux-arm64 \ release-artifacts/safe-chain-linuxstatic-x64 \ release-artifacts/safe-chain-linuxstatic-arm64 \ release-artifacts/safe-chain-win-x64.exe \ release-artifacts/safe-chain-win-arm64.exe \ release-artifacts/install-safe-chain.sh \ release-artifacts/install-safe-chain.ps1 \ release-artifacts/uninstall-safe-chain.sh \ release-artifacts/uninstall-safe-chain.ps1 \ release-artifacts/install-endpoint-mac.sh \ release-artifacts/install-endpoint-windows.ps1 \ release-artifacts/uninstall-endpoint-mac.sh \ release-artifacts/uninstall-endpoint-windows.ps1 publish-homebrew: name: Update Homebrew tap if: github.event_name == 'release' && !github.event.release.prerelease runs-on: ubuntu-latest steps: - name: Extract SHA256 checksums from the released install script env: VERSION: ${{ github.event.release.tag_name }} run: | set -euo pipefail # The release pipeline already bakes per-platform SHA256s into # install-safe-chain.sh. Parse them out instead of re-downloading # ~200MB of binaries just to recompute the same hashes. curl -fsSL --retry 3 \ "https://github.com/AikidoSec/safe-chain/releases/download/${VERSION}/install-safe-chain.sh" \ -o install-safe-chain.sh extract_sha() { grep -E "^${1}=\"[a-f0-9]+\"" install-safe-chain.sh \ | head -n1 \ | sed -E "s/^${1}=\"([a-f0-9]+)\"/\1/" } { echo "SHA_MACOS_X64=$(extract_sha SHA256_MACOS_X64)" echo "SHA_MACOS_ARM64=$(extract_sha SHA256_MACOS_ARM64)" echo "SHA_LINUX_X64=$(extract_sha SHA256_LINUX_X64)" echo "SHA_LINUX_ARM64=$(extract_sha SHA256_LINUX_ARM64)" } >> "$GITHUB_ENV" - name: Verify all checksums were extracted run: | set -euo pipefail missing=() for var in SHA_MACOS_X64 SHA_MACOS_ARM64 SHA_LINUX_X64 SHA_LINUX_ARM64; do if [ -z "${!var:-}" ]; then missing+=("$var") fi done if [ ${#missing[@]} -gt 0 ]; then echo "::error::Could not extract checksum(s) from install-safe-chain.sh: ${missing[*]}" exit 1 fi - name: Checkout Homebrew tap uses: actions/checkout@v4 with: repository: AikidoSec/homebrew-tap path: homebrew-tap token: ${{ secrets.HOMEBREW_TAP_TOKEN }} persist-credentials: true - name: Render formula from template env: VERSION: ${{ github.event.release.tag_name }} run: | set -euo pipefail mkdir -p homebrew-tap/Formula # Quoted heredoc preserves Ruby `#{version}` interpolation and the # backticks inside the caveats string. Placeholders are substituted # with sed below, after the heredoc is written. cat > homebrew-tap/Formula/safe-chain.rb <<'RUBY' # typed: strict # frozen_string_literal: true # This file is auto-generated by AikidoSec/safe-chain's release # workflow. Do not edit by hand — changes will be overwritten on # the next release. # # Aikido Safe Chain wraps your package managers (npm, yarn, pnpm, # pip, poetry, uv, ...) and blocks installations of known-malicious # packages using the Aikido Intel feed. class SafeChain < Formula desc "Block malicious packages from npm, yarn, pnpm, pip, poetry and uv" homepage "https://github.com/AikidoSec/safe-chain" version "__VERSION__" license "AGPL-3.0-or-later" livecheck do url :stable strategy :github_latest end on_macos do on_arm do url "https://github.com/AikidoSec/safe-chain/releases/download/#{version}/safe-chain-macos-arm64", using: :nounzip sha256 "__SHA_MACOS_ARM64__" end on_intel do url "https://github.com/AikidoSec/safe-chain/releases/download/#{version}/safe-chain-macos-x64", using: :nounzip sha256 "__SHA_MACOS_X64__" end end on_linux do on_arm do url "https://github.com/AikidoSec/safe-chain/releases/download/#{version}/safe-chain-linux-arm64", using: :nounzip sha256 "__SHA_LINUX_ARM64__" end on_intel do url "https://github.com/AikidoSec/safe-chain/releases/download/#{version}/safe-chain-linux-x64", using: :nounzip sha256 "__SHA_LINUX_X64__" end end def install binary_name = if OS.mac? Hardware::CPU.arm? ? "safe-chain-macos-arm64" : "safe-chain-macos-x64" else Hardware::CPU.arm? ? "safe-chain-linux-arm64" : "safe-chain-linux-x64" end chmod 0755, binary_name bin.install binary_name => "safe-chain" end def caveats <<~CAVEATS Aikido Safe Chain works by wrapping your package managers (npm, yarn, pnpm, pip, poetry, uv, ...) with shell aliases. After installing, run: safe-chain setup For CI/non-interactive environments, use: safe-chain setup-ci Then restart your shell (or `source ~/.zshrc` / `~/.bashrc`) so the new aliases are picked up. CAVEATS end test do assert_match version.to_s, shell_output("#{bin}/safe-chain --version") assert_match "safe-chain setup", shell_output("#{bin}/safe-chain help") end end RUBY sed -i \ -e "s|__VERSION__|${VERSION}|g" \ -e "s|__SHA_MACOS_ARM64__|${SHA_MACOS_ARM64}|g" \ -e "s|__SHA_MACOS_X64__|${SHA_MACOS_X64}|g" \ -e "s|__SHA_LINUX_ARM64__|${SHA_LINUX_ARM64}|g" \ -e "s|__SHA_LINUX_X64__|${SHA_LINUX_X64}|g" \ homebrew-tap/Formula/safe-chain.rb # Fail fast if any placeholder wasn't substituted (e.g. a new # __XXX__ added to the template without a matching sed rule). if grep -q "__[A-Z_]*__" homebrew-tap/Formula/safe-chain.rb; then echo "::error::Unsubstituted placeholder(s) remain in the formula:" grep -n "__[A-Z_]*__" homebrew-tap/Formula/safe-chain.rb exit 1 fi - name: Commit and push formula update working-directory: homebrew-tap env: VERSION: ${{ github.event.release.tag_name }} run: | set -euo pipefail git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add Formula/safe-chain.rb if git diff --staged --quiet; then echo "No formula changes to commit." exit 0 fi git commit -m "safe-chain ${VERSION}" git push publish-npm: name: Publish to npm if: github.event_name == 'release' runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v3 - name: Set up Node.js uses: actions/setup-node@v3 with: node-version: "lts/*" registry-url: "https://registry.npmjs.org/" - name: Setup safe-chain run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci - name: Set the version in safe-chain package run: npm --no-git-tag-version version ${{ github.event.release.tag_name }} --workspace=packages/safe-chain - name: Install dependencies run: npm ci - name: Run tests run: npm run test - name: Copy documentation files to package run: | cp README.md packages/safe-chain/ cp LICENSE packages/safe-chain/ cp -r docs packages/safe-chain/ cp npm-shrinkwrap.json packages/safe-chain/ - name: Publish to npm run: | VERSION="${{ github.event.release.tag_name }}" echo "Publishing version $VERSION to NPM" if [[ "$VERSION" == *"-"* ]]; then PRERELEASE_TAG=$(echo "$VERSION" | sed 's/.*-\([^-]*\)$/\1/') npm publish --workspace=packages/safe-chain --access public --provenance --tag "$PRERELEASE_TAG" else npm publish --workspace=packages/safe-chain --access public --provenance fi