diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..62911320 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + # Don't pick up brand-new action releases immediately; compromised + # versions are usually yanked within days of publication. + cooldown: + default-days: 7 diff --git a/.github/scripts/render-snapshot-cask.sh b/.github/scripts/render-snapshot-cask.sh new file mode 100755 index 00000000..0a8d86d6 --- /dev/null +++ b/.github/scripts/render-snapshot-cask.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# Renders the jira-cli-snapshot cask from a GoReleaser snapshot dist/. +# GoReleaser OSS only writes casks on real releases, so the snapshot channel +# renders the same shape itself, with checksums from the snapshot build. +# +# Usage: render-snapshot-cask.sh +# The cask is written to stdout. +set -euo pipefail + +DIST="$1" +TAG="$2" + +VERSION=$(jq -r .version "$DIST/metadata.json") + +sha() { + awk -v f="jira_${VERSION}_$1.tar.gz" '$2 == f { print $1 }' "$DIST/checksums.txt" | grep . +} + +MACOS_INTEL=$(sha macOS_x86_64) +MACOS_ARM=$(sha macOS_arm64) +LINUX_INTEL=$(sha linux_x86_64) +LINUX_ARM=$(sha linux_arm64) + +cat <> "$GITHUB_OUTPUT" + + - name: Attest build provenance + uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 + with: + subject-checksums: dist/checksums.txt + + - name: Update snapshot cask in tap + env: + GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + TAG: ${{ steps.publish.outputs.tag }} + # The contents API commits without the token ever touching disk, and + # GitHub signs the commit (shows as verified). + run: | + ./.github/scripts/render-snapshot-cask.sh dist "$TAG" > jira-cli-snapshot.rb + existing=$(gh api repos/rethab/homebrew-jira-cli/contents/Casks/jira-cli-snapshot.rb --jq .sha 2>/dev/null || true) + gh api -X PUT repos/rethab/homebrew-jira-cli/contents/Casks/jira-cli-snapshot.rb \ + -f message="Snapshot cask update for $TAG" \ + -f content="$(base64 -w0 jira-cli-snapshot.rb)" \ + -f "committer[name]=github-actions[bot]" \ + -f "committer[email]=41898282+github-actions[bot]@users.noreply.github.com" \ + ${existing:+-f sha="$existing"} > /dev/null + + - name: Prune old snapshots + env: + GH_TOKEN: ${{ github.token }} + KEEP: '5' + # Tolerates failure: with immutable releases enabled, old snapshots + # cannot be deleted and simply accumulate. + run: | + gh release list --limit 100 --json tagName,createdAt \ + --jq '[.[] | select(.tagName | startswith("snapshot-"))] | sort_by(.createdAt) | reverse | .[].tagName' \ + | tail -n "+$((KEEP + 1))" \ + | while read -r tag; do + gh release delete "$tag" --cleanup-tag --yes \ + || echo "could not prune $tag (immutable releases?)" + done diff --git a/.goreleaser.yml b/.goreleaser.yml index b16552be..463ddfb8 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -4,17 +4,22 @@ project_name: jira release: prerelease: auto name_template: "v{{.Version}}" - draft: true + draft: false mode: "keep-existing" -before: - hooks: - - go mod tidy +snapshot: + # Monotonic so Homebrew never sees a snapshot as a downgrade. + version_template: "{{ incpatch .Version }}-next.{{ .CommitTimestamp }}.{{ .ShortCommit }}" builds: - <<: &build_defaults binary: bin/jira main: ./cmd/jira + # Reproducible builds: strip local paths and pin file timestamps to the + # commit, so anyone can rebuild the tag and compare checksums. + flags: + - -trimpath + mod_timestamp: "{{ .CommitTimestamp }}" ldflags: - -s -w - -X github.com/ankitpokhrel/jira-cli/internal/version.Version={{.Version}} @@ -59,50 +64,61 @@ checksum: name_template: 'checksums.txt' algorithm: sha256 -brews: +sboms: + - artifacts: archive + +homebrew_casks: - name: jira-cli - homepage: "https://github.com/ankitpokhrel/jira-cli" + homepage: "https://github.com/rethab/jira-cli" description: "🔥 Feature-rich interactive Jira command-line" license: "MIT" ids: - nix + # The archives ship the binary under bin/, so the cask must reference it there. + binaries: + - bin/jira + + generate_completions_from_executable: + executable: bin/jira + args: + - completion + shells: + - bash + - zsh + - fish + + url: + verified: "github.com/rethab/jira-cli/" + + conflicts: + - cask: jira-cli-snapshot + + hooks: + post: + # The released binaries are neither signed nor notarized, so Gatekeeper + # quarantines them on download and every run would be blocked. + install: | + if OS.mac? + system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/bin/jira"] + end + repository: - owner: ankitpokhrel + owner: rethab name: homebrew-jira-cli - - custom_block: | - head do - url "https://github.com/ankitpokhrel/jira-cli.git", branch: "main" - depends_on "go" - end - - install: | - if build.head? - system "make", "install" - bin.install ENV["GOPATH"] + "/bin/jira" - else - bin.install File.exist?("bin/jira") ? "bin/jira" : "jira" - end - generate_completions_from_executable(bin/"jira", "completion", shells: [:bash, :zsh, :fish]) - - test: | - help_text = shell_output("#{bin}/jira version") - assert_includes help_text, "Version=\"#{version}\"" + # The default GITHUB_TOKEN is scoped to this repository only, so pushing + # the cask to the tap needs a token that can write to it. + token: "{{ .Env.HOMEBREW_TAP_TOKEN }}" commit_author: - name: ankitpokhrel - email: oss@ankit.pl - - commit_msg_template: "Formula update for version {{ .Tag }}" + name: github-actions[bot] + email: 41898282+github-actions[bot]@users.noreply.github.com - # Folder inside the repository to put the formula to. - directory: Formula + commit_msg_template: "Cask update for version {{ .Tag }}" - # This will prevent goreleaser to actually try to commit the updated - # formula - leaving the responsibility of publishing it to the user. - skip_upload: true + # Folder inside the tap repository to put the cask into. + directory: Casks changelog: disable: true diff --git a/Makefile b/Makefile index 2b99338d..1c305093 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,7 @@ install: lint: @if ! command -v golangci-lint > /dev/null 2>&1; then \ - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | \ + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/v2.6.2/install.sh | \ sh -s -- -b "$$(go env GOPATH)/bin" v2.6.2 ; \ fi golangci-lint run ./... diff --git a/README.md b/README.md index 28a16856..ecd027ba 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,27 @@ nature of the data. Yet, we've attempted to make the experience as similar as po | **Jira** | Jira CloudJira Server | ## Installation -`jira-cli` is available as a downloadable packaged binary for Linux, macOS, and Windows from the [releases page](https://github.com/ankitpokhrel/jira-cli/releases). +`jira-cli` is available as a downloadable packaged binary for Linux, macOS, and Windows from the [releases page](https://github.com/rethab/jira-cli/releases). + +Install it with Homebrew from the [tap](https://github.com/rethab/homebrew-jira-cli): + +```sh +brew install --cask rethab/jira-cli/jira-cli +``` + +Every release carries [SLSA build provenance](https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations): a signed attestation that the archive was built by this repository's release workflow from the tagged commit. To verify a downloaded archive: + +```sh +gh attestation verify jira_1.8.0_macOS_arm64.tar.gz --repo rethab/jira-cli +``` + +Each archive also ships an SPDX SBOM (`*.sbom.json` on the release page) listing the full dependency tree, ready for scanners like `grype`. + +Every merge to `main` also publishes a snapshot build with the same checksums, provenance, and SBOMs as a release: + +```sh +brew install --cask rethab/jira-cli/jira-cli-snapshot +``` You can use Docker to quickly try out `jira-cli`.