diff --git a/.github/scripts/ephemeral-crate.sh b/.github/scripts/ephemeral-crate.sh new file mode 100755 index 00000000..3636ca71 --- /dev/null +++ b/.github/scripts/ephemeral-crate.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +cat >> crates/bin/Cargo.toml <> crates/bin/Cargo.toml <> "$GITHUB_OUTPUT" -cp minisign.pub crates/bin/minisign.pub - set +x echo "::add-mask::$(tail -n1 minisign.key)" -echo "private=$(tail -n1 minisign.key)" >> "$GITHUB_OUTPUT" set -x +rage --encrypt --recipient "$AGE_KEY_PUBLIC" --output minisign.key.age minisign.key rm minisign.key diff --git a/.github/scripts/ephemeral-sign.sh b/.github/scripts/ephemeral-sign.sh index a657c915..ef458e58 100755 --- a/.github/scripts/ephemeral-sign.sh +++ b/.github/scripts/ephemeral-sign.sh @@ -2,14 +2,15 @@ set -euo pipefail -echo "untrusted comment: rsign encrypted secret key" > minisign.key -cat >> minisign.key <<< "$SIGNING_KEY" +[[ -z "$AGE_KEY_SECRET" ]] && { echo "!!! Empty age key secret !!!"; exit 1; } +cat >> age.key <<< "$AGE_KEY_SECRET" set -x -cargo binstall -y rsign2 +cargo binstall -y rsign2 rage +rage --decrypt --identity age.key --output minisign.key minisign.key.age -ts=$(date --utc --iso-8601=seconds) +ts=$(node -e 'console.log((new Date).toISOString())') git=$(git rev-parse HEAD) comment="gh=$GITHUB_REPOSITORY git=$git ts=$ts run=$GITHUB_RUN_ID" @@ -17,3 +18,4 @@ for file in "$@"; do rsign sign -W -s minisign.key -x "$file.sig" -t "$comment" "$file" done +rm age.key minisign.key diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c10c8f3..dd31ba61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -125,9 +125,17 @@ jobs: - run: just avoid-dev-deps - run: just lint - release-builds: - uses: ./.github/workflows/release-build.yml + release-dry-run: + uses: ./.github/workflows/release-cli.yml + secrets: inherit with: + info: | + { + "is-release": false, + "crate": "cargo-binstall", + "version": "0.0.0", + "notes": "" + } CARGO_PROFILE_RELEASE_LTO: no CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 4 @@ -179,7 +187,7 @@ jobs: - test - cross-check - lint - - release-builds + - release-dry-run - detect-targets-alpine-test - detect-targets-ubuntu-test if: always() # always run even if dependencies fail diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml new file mode 100644 index 00000000..bec98814 --- /dev/null +++ b/.github/workflows/release-cli.yml @@ -0,0 +1,108 @@ +name: Release CLI +on: + workflow_call: + inputs: + info: + description: "The release metadata JSON" + required: true + type: string + CARGO_PROFILE_RELEASE_LTO: + description: "Used to speed up CI" + required: false + type: string + CARGO_PROFILE_RELEASE_CODEGEN_UNITS: + description: "Used to speed up CI" + required: false + type: string + +jobs: + tag: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - if: fromJSON(inputs.info).is-release == 'true' + name: Push cli release tag + uses: mathieudutour/github-tag-action@v6.1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + custom_tag: ${{ fromJSON(inputs.info).version }} + tag_prefix: v + + keygen: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: cargo-bins/cargo-binstall@main + - name: Create ephemeral keypair + id: keypair + env: + AGE_KEY_PUBLIC: ${{ vars.AGE_KEY_PUBLIC }} + run: .github/scripts/ephemeral-gen.sh + - uses: actions/upload-artifact@v3 + with: + name: minisign.pub + path: minisign.pub + - uses: actions/upload-artifact@v3 + with: + name: minisign.key.age + path: minisign.key.age + retention-days: 1 + - name: Check that key can be decrypted + env: + AGE_KEY_SECRET: ${{ secrets.AGE_KEY_SECRET }} + shell: bash + run: .github/scripts/ephemeral-sign.sh minisign.pub + + package: + needs: + - tag + - keygen + uses: ./.github/workflows/release-packages.yml + secrets: inherit + with: + publish: ${{ inputs.info }} + CARGO_PROFILE_RELEASE_LTO: ${{ inputs.CARGO_PROFILE_RELEASE_LTO }} + CARGO_PROFILE_RELEASE_CODEGEN_UNITS: ${{ inputs.CARGO_PROFILE_RELEASE_CODEGEN_UNITS }} + + publish: + needs: package + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v3 + with: + name: minisign.pub + - run: .github/scripts/ephemeral-crate.sh + + - if: fromJSON(inputs.info).is-release != 'true' + name: DRY-RUN Publish to crates.io + env: + crate: ${{ fromJSON(inputs.info).crate }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish -p "$crate" --allow-dirty --dry-run + + - if: fromJSON(inputs.info).is-release == 'true' + name: Publish to crates.io + env: + crate: ${{ fromJSON(inputs.info).crate }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish -p "$crate" --allow-dirty + + - if: fromJSON(inputs.info).is-release == 'true' + name: Make release latest + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + release_name: v${{ fromJSON(inputs.info).version }} + tag: v${{ fromJSON(inputs.info).version }} + body: ${{ fromJSON(inputs.info).notes }} + promote: true + file: minisign.pub + + - if: fromJSON(inputs.info).is-release == 'true' + name: Delete signing key artifact + uses: geekyeggo/delete-artifact@v2 + with: + name: minisign.key.age + failOnError: false + diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-packages.yml similarity index 76% rename from .github/workflows/release-build.yml rename to .github/workflows/release-packages.yml index ae7f5f4d..ab6839c6 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-packages.yml @@ -1,29 +1,20 @@ -name: Build for release +name: Build packages for release on: - workflow_dispatch: # can't publish from dispatch workflow_call: inputs: publish: - description: "Set to the release metadata JSON to publish the release" - required: false - type: string - publickey: - description: "Minisign public key. Required when publishing" - required: false + description: "The release metadata JSON" + required: true type: string CARGO_PROFILE_RELEASE_LTO: - description: "Set to override default release profile lto settings" + description: "Used to speed up CI" required: false type: string CARGO_PROFILE_RELEASE_CODEGEN_UNITS: - description: "Set to override default release profile codegen-units settings" + description: "Used to speed up CI" required: false type: string - secrets: - signingkey: - description: "Minisign private key. Required when publishing" - required: false env: CARGO_TERM_COLOR: always @@ -69,15 +60,7 @@ jobs: if: inputs.CARGO_PROFILE_RELEASE_CODEGEN_UNITS run: echo "CARGO_PROFILE_RELEASE_CODEGEN_UNITS=${{ inputs.CARGO_PROFILE_RELEASE_CODEGEN_UNITS }}" >> "$GITHUB_ENV" - - name: Include public key in package - if: inputs.publickey - env: - PUBLIC_KEY: ${{ inputs.publickey }} - shell: bash - run: | - echo "untrusted comment: minisign public key" > minisign.pub - cat >> minisign.pub <<< "$PUBLIC_KEY" - + - uses: cargo-bins/cargo-binstall@main - uses: ./.github/actions/just-setup with: tools: cargo-auditable @@ -89,6 +72,9 @@ jobs: - run: just toolchain rust-src - run: just ci-install-deps + - uses: actions/download-artifact@v3 + with: + name: minisign.pub - run: just package - if: runner.os == 'Windows' run: Get-ChildItem packages/ @@ -101,16 +87,16 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - if: inputs.publish - uses: cargo-bins/cargo-binstall@main - - - if: inputs.publish + - uses: actions/download-artifact@v3 + with: + name: minisign.key.age + - name: Sign package env: - SIGNING_KEY: ${{ secrets.signingkey }} + AGE_KEY_SECRET: ${{ secrets.AGE_KEY_SECRET }} shell: bash run: .github/scripts/ephemeral-sign.sh packages/cargo-binstall-* - - if: inputs.publish + - if: fromJSON(inputs.publish).is-release == 'true' name: Upload to release uses: svenstaro/upload-release-action@v2 with: @@ -120,7 +106,8 @@ jobs: body: ${{ fromJSON(inputs.publish).notes }} file: packages/cargo-binstall-* file_glob: true - - if: "! inputs.publish || runner.os == 'macOS'" + prerelease: true + - if: "fromJSON(inputs.publish).is-release != 'true' || runner.os == 'macOS'" name: Upload artifact uses: actions/upload-artifact@v3 with: @@ -144,16 +131,7 @@ jobs: steps: - uses: actions/checkout@v4 - - - name: Include public key in package - if: inputs.publickey - env: - PUBLIC_KEY: ${{ inputs.publickey }} - shell: bash - run: | - echo "untrusted comment: minisign public key" > minisign.pub - cat >> minisign.pub <<< "$PUBLIC_KEY" - + - uses: cargo-bins/cargo-binstall@main - uses: taiki-e/install-action@v2 with: tool: just @@ -171,19 +149,22 @@ jobs: name: aarch64-apple-darwin path: packages/ + - uses: actions/download-artifact@v3 + with: + name: minisign.pub - run: ls -shalr packages/ - run: just repackage-lipo - run: ls -shal packages/ - - if: inputs.publish - uses: cargo-bins/cargo-binstall@main - - - if: inputs.publish - env: - SIGNING_KEY: ${{ secrets.signingkey }} + - uses: actions/download-artifact@v3 + with: + name: minisign.key.age + - env: + AGE_KEY_SECRET: ${{ secrets.AGE_KEY_SECRET }} + shell: bash run: .github/scripts/ephemeral-sign.sh packages/cargo-binstall-universal-* - - if: inputs.publish + - if: fromJSON(inputs.publish).is-release == 'true' name: Upload to release uses: svenstaro/upload-release-action@v2 with: @@ -194,7 +175,8 @@ jobs: file: packages/cargo-binstall-universal-* file_glob: true overwrite: true - - if: "! inputs.publish" + prerelease: true + - if: fromJSON(inputs.publish).is-release != 'true' name: Upload artifact uses: actions/upload-artifact@v3 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7b189b2d..88f6d0a7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: event-data: ${{ toJSON(github.event) }} extract-notes-under: '### Release notes' - libtag: + release-lib: if: needs.info.outputs.is-release == 'true' && needs.info.outputs.crate != 'cargo-binstall' needs: info runs-on: ubuntu-latest @@ -41,47 +41,11 @@ jobs: env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - clitag: - if: needs.info.outputs.is-release == 'true' && needs.info.outputs.crate == 'cargo-binstall' + release-cli: + if: needs.info.outputs.crate == 'cargo-binstall' needs: info - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Push cli release tag - uses: mathieudutour/github-tag-action@v6.1 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - custom_tag: ${{ needs.info.outputs.version }} - tag_prefix: v - - uses: cargo-bins/cargo-binstall@main - - name: Create ephemeral keypair - id: keypair - run: .github/scripts/ephemeral-gen.sh - - name: Publish to crates.io - env: - crate: ${{ needs.info.outputs.crate }} - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - run: cargo publish -p "$crate" --allow-dirty - - name: Upload public key to release - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - release_name: v${{ needs.info.outputs.version }} - tag: v${{ needs.info.outputs.version }} - body: ${{ needs.info.outputs.notes }} - file: minisign.pub - outputs: - publickey: ${{ steps.keypair.outputs.public }} - signingkey: ${{ steps.keypair.outputs.private }} - - package: - if: needs.info.outputs.is-release == 'true' && needs.info.outputs.crate == 'cargo-binstall' - needs: - - info - - clitag - uses: ./.github/workflows/release-build.yml + uses: ./.github/workflows/release-cli.yml + secrets: inherit with: - publish: ${{ toJSON(needs.info.outputs) }} - publickey: ${{ needs.clitag.publickey }} - secrets: - signingkey: ${{ needs.clitag.signingkey }} + info: ${{ toJSON(needs.info.outputs) }} + diff --git a/SIGNING.md b/SIGNING.md index e8cc2871..29803ce4 100644 --- a/SIGNING.md +++ b/SIGNING.md @@ -10,10 +10,10 @@ This feature requires adding to the Cargo.toml metadata: no autodiscovery here! Generate a [minisign](https://jedisct1.github.io/minisign/) keypair: ```console -minisign -G -p signing.pub -s signing.key +minisign -G -W -p signing.pub -s signing.key # or with rsign2: -rsign generate -p signing.pub -s signing.key +rsign generate -W -p signing.pub -s signing.key ``` In your Cargo.toml, put: @@ -31,10 +31,10 @@ Save the `signing.key` as a secret in your CI, then use it when building package ```console tar cvf package-name.tar.zst your-files # or however -minisign -S -s signing.key -x package-name.tar.zst.sig -m package-name.tar.zst +minisign -S -W -s signing.key -x package-name.tar.zst.sig -m package-name.tar.zst # or with rsign2: -rsign sign -s signing.key -x package-name.tar.zst.sig package-name.tar.zst +rsign sign -W -s signing.key -x package-name.tar.zst.sig package-name.tar.zst ``` Upload both your package and the matching `.sig`. @@ -42,34 +42,16 @@ Upload both your package and the matching `.sig`. Now when binstall downloads your packages, it will also download the `.sig` file and use the `pubkey` in the Cargo.toml to verify the signature. If the signature has a trusted comment, it will print it at install time. -`minisign` and `rsign2` by default prompt for a password when generating a keypair and signing, which can hinder automation. +By default, `minisign` and `rsign2` prompt for a password; above we disable this with `-W`. +While you _can_ set a password, we recommend instead using [age](https://github.com/FiloSottile/age) (or the Rust version [rage](https://github.com/str4d/rage)) to separately encrypt the key, which we find is much better for automation. -You can: - - Pass `-W` to `minisign` or `rsign2` to generate a password-less private key. - NOTE that you also need to pass this when signing. - - When signing using `minisign`, it reads from stdin for password so you could use - shell redirect to pass the password. - - Use [`expect`] to pass password to `rsign2` (since it reads `/dev/tty` for password): - For generating private key: - ```bash - expect <