diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 82e929f..54b3df0 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -105,3 +105,16 @@ jobs: exit 1 fi test ! -e "$RUNNER_TEMP/bad-bin/devlyft" + - name: deb package installs + run: | + version=$(jq -r .version dist/metadata.json) + sudo dpkg -i "dist/devlyft_${version}_linux_amd64.deb" + devlyft version | grep -F "devlyft $version" + sudo dpkg -r devlyft + - name: rpm package installs + run: | + version=$(jq -r .version dist/metadata.json) + docker run --rm -v "$PWD/dist:/dist:ro" fedora:latest sh -c \ + "rpm -i /dist/devlyft_${version}_linux_amd64.rpm && devlyft version | grep -F 'devlyft $version'" + - name: Scoop manifest is valid JSON with both architectures + run: jq -e '.architecture["64bit"].hash and .architecture.arm64.hash' dist/scoop/bucket/devlyft.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0b29db4..8ea5e5a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,9 +42,14 @@ jobs: # Write access to devlyft/homebrew-tap only. When unset, the cask # is built but not pushed. HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} + # Write access to devlyft/scoop-bucket only. When unset, the + # manifest is built but not pushed. + SCOOP_BUCKET_GITHUB_TOKEN: ${{ secrets.SCOOP_BUCKET_GITHUB_TOKEN }} - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-path: | dist/*.tar.gz dist/*.zip + dist/*.deb + dist/*.rpm dist/*_checksums.txt diff --git a/.goreleaser.yaml b/.goreleaser.yaml index a5b7bce..84746fa 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -97,6 +97,56 @@ homebrew_casks: run "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "{{ .StagedPath }}/devlyft"] end +# Linux packages, attached to the GitHub release and covered by the signed +# checksum file. +nfpms: + - id: devlyft + ids: [devlyft] + package_name: devlyft + file_name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" + formats: [deb, rpm] + vendor: DevLyft + homepage: https://github.com/devlyft/cli + maintainer: DevLyft + description: The official command-line interface for DevLyft. + license: Apache-2.0 + section: devel + bindir: /usr/bin + # Reproducible packages: timestamps come from the commit, not the clock. + mtime: "{{ .CommitDate }}" + contents: + - src: LICENSE + dst: /usr/share/doc/devlyft/LICENSE + file_info: + mode: 0644 + - src: README.md + dst: /usr/share/doc/devlyft/README.md + file_info: + mode: 0644 + rpm: + group: Development/Tools + +# Scoop: `scoop bucket add devlyft https://github.com/devlyft/scoop-bucket` +# then `scoop install devlyft`. The manifest is pushed with a token scoped to +# that repository only; without the token the upload is skipped instead of +# failing the release. +scoops: + - name: devlyft + ids: [devlyft] + description: The official command-line interface for DevLyft. + homepage: https://github.com/devlyft/cli + license: Apache-2.0 + repository: + owner: devlyft + name: scoop-bucket + token: '{{ envOrDefault "SCOOP_BUCKET_GITHUB_TOKEN" "" }}' + directory: bucket + commit_author: + name: devlyft-release-bot + email: noreply@devlyft.io + commit_msg_template: "Scoop update for {{ .ProjectName }} version {{ .Tag }}" + skip_upload: '{{ if not (envOrDefault "SCOOP_BUCKET_GITHUB_TOKEN" "") }}true{{ end }}' + # release-please writes the changelog and the release notes. changelog: disable: true diff --git a/README.md b/README.md index 4effec3..701eb97 100644 --- a/README.md +++ b/README.md @@ -17,13 +17,27 @@ curl -fsSL https://raw.githubusercontent.com/devlyft/cli/main/install.sh | sh Set `DEVLYFT_VERSION` to pin a version, `DEVLYFT_INSTALL_DIR` to choose where the binary goes (default `/usr/local/bin` if writable, otherwise `~/.local/bin`), and `DEVLYFT_REQUIRE_SIGNATURE=1` to refuse to install without a verified signature. +**Scoop** (Windows): + +```powershell +scoop bucket add devlyft https://github.com/devlyft/scoop-bucket +scoop install devlyft +``` + +**Debian, Ubuntu, Fedora, RHEL**: download the `.deb` or `.rpm` for your architecture from [Releases](https://github.com/devlyft/cli/releases), then: + +```sh +sudo dpkg -i devlyft__linux_amd64.deb # Debian, Ubuntu +sudo rpm -i devlyft__linux_amd64.rpm # Fedora, RHEL +``` + **Go**: ```sh go install github.com/devlyft/cli/cmd/devlyft@latest ``` -Or download an archive from [Releases](https://github.com/devlyft/cli/releases); see [docs/releasing.md](docs/releasing.md#verifying-a-download) to verify it. +Or download an archive or package from [Releases](https://github.com/devlyft/cli/releases). Every release file is listed in a cosign-signed checksum file; see [docs/releasing.md](docs/releasing.md#verifying-a-download) to verify a download. ## Development diff --git a/docs/releasing.md b/docs/releasing.md index 6ba048a..5881ff7 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -10,13 +10,15 @@ Releases are automated. Nobody tags by hand. 4. The tag triggers `.github/workflows/release.yml`, which runs [goreleaser](https://goreleaser.com/) on a GitHub-hosted runner to: - build `devlyft` for Linux, macOS and Windows on amd64 and arm64 (`CGO_ENABLED=0`, `-trimpath`, version, commit and date stamped in); - package `.tar.gz` archives (`.zip` on Windows) with an SPDX SBOM per archive, generated by syft; - - write `devlyft__checksums.txt` covering every archive and SBOM; + - build `.deb` and `.rpm` packages for Linux amd64 and arm64 with [nfpm](https://nfpm.goreleaser.com/) (binary in `/usr/bin`, licence and README in `/usr/share/doc/devlyft`); + - write `devlyft__checksums.txt` covering every archive, package and SBOM; - sign the checksum file with [cosign](https://docs.sigstore.dev/) keyless signing, using the workflow's GitHub OIDC identity (no stored keys), producing `.sig`, `.pem` and `.sigstore.json`; - - attach everything to the release. + - attach everything to the release; + - push a Scoop manifest (`bucket/devlyft.json`) to [devlyft/scoop-bucket](https://github.com/devlyft/scoop-bucket) using the `SCOOP_BUCKET_GITHUB_TOKEN` secret. When that secret is not set, the manifest is built but not pushed and the release still succeeds. 5. goreleaser also generates a Homebrew cask and pushes it to [devlyft/homebrew-tap](https://github.com/devlyft/homebrew-tap) (`Casks/devlyft.rb`), so `brew install devlyft/tap/devlyft` picks up the release. The push uses the `HOMEBREW_TAP_GITHUB_TOKEN` secret; when that secret is not set, the cask is built but not pushed and the release still succeeds. The cask installs bash, zsh and fish completions by running `devlyft completion `, and on macOS clears the quarantine attribute because the binary is not notarized. -6. The workflow then records a GitHub build-provenance attestation for the archives and the checksum file. +6. The workflow then records a GitHub build-provenance attestation for the archives, the Linux packages and the checksum file. -Every pull request also runs `checks / release-snapshot`, which builds all targets with the same config but skips signing, SBOMs and publishing, then runs `install.sh` against the snapshot (and checks it rejects a tampered archive). Run the same thing locally with `make snapshot`; the output lands in `dist/`. +Every pull request also runs `checks / release-snapshot`, which builds all targets with the same config but skips signing, SBOMs and publishing, then runs `install.sh` against the snapshot (and checks it rejects a tampered archive), installs the `.deb` on the runner and the `.rpm` in a Fedora container, and checks the Scoop manifest. Run the same thing locally with `make snapshot`; the output lands in `dist/`. ### Setting up the Homebrew tap @@ -26,6 +28,18 @@ One-off, before the first release that should reach Homebrew: 2. Create a credential that can write only to that repository: a fine-grained personal access token owned by a bot account (or a GitHub App installation token) with **Contents: read and write** on `devlyft/homebrew-tap` and nothing else. 3. Store it as the `HOMEBREW_TAP_GITHUB_TOKEN` secret in `devlyft/cli`, ideally in a protected environment used only by the release workflow. +### Setting up the Scoop bucket + +One-off, before the first release that should reach Scoop: + +1. Create the public repository `devlyft/scoop-bucket` with a README and a `bucket/` directory. +2. Create a credential that can write only to that repository: a fine-grained personal access token owned by a bot account (or a GitHub App installation token) with **Contents: read and write** on `devlyft/scoop-bucket` and nothing else. +3. Store it as the `SCOOP_BUCKET_GITHUB_TOKEN` secret in `devlyft/cli`, ideally in a protected environment used only by the release workflow. + +### Linux packages + +The `.deb` and `.rpm` files are attached to each GitHub release. They are not GPG-signed and there is no apt or yum repository yet; verify them through the signed checksum file like any other download (see below), then `sudo dpkg -i` or `sudo rpm -i` them. Shell completions and man pages are not packaged yet; man pages come with devlyft/devlyft#248. + ## The install script `install.sh` (served from `main` via raw.githubusercontent.com) installs on Linux and macOS: @@ -75,5 +89,5 @@ To cut 1.0, merge a commit containing `Release-As: 1.0.0` in its body; release-p ## Known gaps - macOS binaries are not notarized and Windows binaries are not code-signed, so browser downloads trigger Gatekeeper and SmartScreen warnings. Installing through Homebrew or the install script avoids this. -- Scoop, deb and rpm channels are tracked separately (devlyft/devlyft#260). - The install script supports Linux and macOS only; Windows users install with Scoop or the zip archive. +- The `.deb` and `.rpm` packages are not GPG-signed and are not served from an apt or yum repository.