diff --git a/.github/workflows/go-releaser-checks.yml b/.github/workflows/go-releaser-checks.yml index 999e178..eb513c6 100644 --- a/.github/workflows/go-releaser-checks.yml +++ b/.github/workflows/go-releaser-checks.yml @@ -16,17 +16,36 @@ jobs: with: fetch-depth: 0 + - name: Read build metadata + id: build_info + run: | + bash ./scripts/read_build_info.sh >> "$GITHUB_OUTPUT" + + - name: Reject preview-tag in pull requests to main + if: ${{ github.event_name == 'pull_request' && github.base_ref == 'main' }} + run: | + if [[ -n "${{ steps.build_info.outputs.preview_tag }}" ]]; then + echo "preview-tag must be empty for pull requests targeting main." >&2 + exit 1 + fi + - name: Check GoReleaser - uses: goreleaser/goreleaser-action@v6 + uses: goreleaser/goreleaser-action@v7 with: distribution: goreleaser version: '~> v2' args: check + env: + TERRABLE_BUILD_VERSION: ${{ steps.build_info.outputs.version }} + TERRABLE_BUILD_PREVIEW_TAG: ${{ steps.build_info.outputs.preview_tag }} - name: GoReleaser dry-run - uses: goreleaser/goreleaser-action@v6 + uses: goreleaser/goreleaser-action@v7 with: distribution: goreleaser version: '~> v2' args: build --snapshot - \ No newline at end of file + env: + TERRABLE_BUILD_VERSION: ${{ steps.build_info.outputs.version }} + TERRABLE_BUILD_PREVIEW_TAG: ${{ steps.build_info.outputs.preview_tag }} + diff --git a/.github/workflows/go-releaser-release.yml b/.github/workflows/go-releaser-release.yml index 298bc25..9e896e8 100644 --- a/.github/workflows/go-releaser-release.yml +++ b/.github/workflows/go-releaser-release.yml @@ -12,12 +12,28 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + env: + HOMEBREW_TAP_BRANCH: ${{ github.ref_type == 'tag' && contains(github.ref_name, '-') && 'preview' || 'main' }} steps: - name: Check out repository uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Read build metadata + id: build_info + run: | + bash ./scripts/read_build_info.sh >> "$GITHUB_OUTPUT" + + - name: Select GoReleaser mode + id: goreleaser_mode + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + echo "args=release --snapshot --clean" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "args=release --clean" >> "$GITHUB_OUTPUT" + - name: Generate GitHub App token id: generate_token uses: tibdex/github-app-token@v2 @@ -33,11 +49,13 @@ jobs: passphrase: ${{ secrets.GPG_PASSPHRASE }} - name: GoReleaser release - uses: goreleaser/goreleaser-action@v6 + uses: goreleaser/goreleaser-action@v7 with: distribution: goreleaser version: '~> v2' - args: release --clean + args: ${{ steps.goreleaser_mode.outputs.args }} env: GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} + TERRABLE_BUILD_VERSION: ${{ steps.build_info.outputs.version }} + TERRABLE_BUILD_PREVIEW_TAG: ${{ steps.build_info.outputs.preview_tag }} diff --git a/.github/workflows/publish-new-version-tag.yml b/.github/workflows/publish-new-version-tag.yml index 66eeb95..8189715 100644 --- a/.github/workflows/publish-new-version-tag.yml +++ b/.github/workflows/publish-new-version-tag.yml @@ -27,18 +27,39 @@ jobs: fetch-depth: 0 token: ${{ steps.generate_token.outputs.token }} - - name: Read version from file - id: version + - name: Read build metadata + id: build_info run: | - VERSION=$(grep -oP 'version\s*=\s*\K[\d.]+' terrable_build) - echo "::set-output name=version::$VERSION" + bash ./scripts/read_build_info.sh >> "$GITHUB_OUTPUT" + + - name: Reject preview-tag on main + run: | + if [[ -n "${{ steps.build_info.outputs.preview_tag }}" ]]; then + echo "preview-tag must be empty on main releases." >&2 + exit 1 + fi + + - name: Check whether stable tag already exists + id: tag + run: | + TAG="v${{ steps.build_info.outputs.version }}" + if git rev-parse "$TAG" >/dev/null 2>&1; then + EXISTING_COMMIT=$(git rev-list -n1 "$TAG") + if [[ "$EXISTING_COMMIT" == "$GITHUB_SHA" ]]; then + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "Tag $TAG already exists at $EXISTING_COMMIT. Bump terrable_build version before releasing again." >&2 + exit 1 + fi + echo "skip=false" >> "$GITHUB_OUTPUT" - name: Create and push tag + if: ${{ steps.tag.outputs.skip != 'true' }} env: GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} run: | git config user.name goreleaserbot git config user.email bot@goreleaser.com - git tag -a v${{ steps.version.outputs.version }} -m "Version ${{ steps.version.outputs.version }}" - git push https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git v${{ steps.version.outputs.version }} - \ No newline at end of file + git tag -a v${{ steps.build_info.outputs.version }} -m "Version ${{ steps.build_info.outputs.version }}" + git push https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git v${{ steps.build_info.outputs.version }} diff --git a/.github/workflows/publish-preview-version-tag.yml b/.github/workflows/publish-preview-version-tag.yml new file mode 100644 index 0000000..76e1cdf --- /dev/null +++ b/.github/workflows/publish-preview-version-tag.yml @@ -0,0 +1,76 @@ +name: Publish Preview Version Tag + +on: + push: + branches-ignore: + - main + paths: + - 'terrable_build' + +jobs: + publish-preview-version-tag: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Read build metadata + id: build_info + run: | + bash ./scripts/read_build_info.sh >> "$GITHUB_OUTPUT" + + - name: Decide whether this branch should publish a preview + id: preview + run: | + if [[ -z "${{ steps.build_info.outputs.preview_tag }}" ]]; then + echo "publish=false" >> "$GITHUB_OUTPUT" + echo "No preview-tag set in terrable_build, so no preview tag will be published." + exit 0 + fi + echo "publish=true" >> "$GITHUB_OUTPUT" + + - name: Generate GitHub App token + if: ${{ steps.preview.outputs.publish == 'true' }} + id: generate_token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.ACTIONS_APP_ID }} + private_key: ${{ secrets.ACTIONS_APP_PRIVATE_KEY }} + + - name: Check out repository with app token + if: ${{ steps.preview.outputs.publish == 'true' }} + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ steps.generate_token.outputs.token }} + + - name: Check whether preview tag already exists + if: ${{ steps.preview.outputs.publish == 'true' }} + id: tag + run: | + TAG="v${{ steps.build_info.outputs.version }}-${{ steps.build_info.outputs.preview_tag }}" + echo "name=$TAG" >> "$GITHUB_OUTPUT" + if git rev-parse "$TAG" >/dev/null 2>&1; then + EXISTING_COMMIT=$(git rev-list -n1 "$TAG") + if [[ "$EXISTING_COMMIT" == "$GITHUB_SHA" ]]; then + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "Tag $TAG already exists at $EXISTING_COMMIT. Bump preview-tag in terrable_build before publishing a new preview." >&2 + exit 1 + fi + echo "skip=false" >> "$GITHUB_OUTPUT" + + - name: Create and push preview tag + if: ${{ steps.preview.outputs.publish == 'true' && steps.tag.outputs.skip != 'true' }} + env: + GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} + run: | + git config user.name goreleaserbot + git config user.email bot@goreleaser.com + git tag -a "${{ steps.tag.outputs.name }}" -m "Preview ${{ steps.tag.outputs.name }}" + git push https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git "${{ steps.tag.outputs.name }}" diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 1f7550f..48b1c0a 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -19,6 +19,12 @@ signs: checksum: name_template: "checksums.txt" +snapshot: + version_template: "{{ .Env.TERRABLE_BUILD_VERSION }}{{ if .Env.TERRABLE_BUILD_PREVIEW_TAG }}-{{ .Env.TERRABLE_BUILD_PREVIEW_TAG }}{{ end }}-snapshot-{{ .ShortCommit }}" + +release: + prerelease: auto + archives: - formats: [ tar.gz ] name_template: >- @@ -45,6 +51,7 @@ scoops: - name: terrable url_template: "https://github.com/terrable-dev/terrable/releases/download/{{ .Tag }}/{{ .ArtifactName }}" directory: bucket + skip_upload: auto commit_author: name: goreleaserbot email: bot@goreleaser.com @@ -60,7 +67,7 @@ homebrew_casks: - name: terrable url: template: "https://github.com/terrable-dev/terrable/releases/download/{{ .Tag }}/{{ .ArtifactName }}" - directory: Formula + directory: Casks commit_author: name: goreleaserbot email: bot@goreleaser.com @@ -70,4 +77,4 @@ homebrew_casks: repository: owner: terrable-dev name: homebrew-tap - branch: main + branch: "{{ envOrDefault \"HOMEBREW_TAP_BRANCH\" \"main\" }}" diff --git a/main.go b/main.go index 1e1f246..7483906 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,7 @@ import ( func main() { app := &cli.App{ Name: "terrable", - Version: buildInfo()["version"], + Version: buildVersion(), Commands: []*cli.Command{ { diff --git a/scripts/read_build_info.sh b/scripts/read_build_info.sh new file mode 100755 index 0000000..41c359a --- /dev/null +++ b/scripts/read_build_info.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +set -euo pipefail + +file="${1:-terrable_build}" + +read_key() { + local key="$1" + + awk -F= -v key="$key" ' + $0 ~ /^[[:space:]]*#/ || $0 ~ /^[[:space:]]*$/ { next } + { + current_key = $1 + sub(/^[[:space:]]+/, "", current_key) + sub(/[[:space:]]+$/, "", current_key) + + if (current_key != key) { + next + } + + value = substr($0, index($0, "=") + 1) + sub(/^[[:space:]]+/, "", value) + sub(/[[:space:]]+$/, "", value) + + if ((value ~ /^".*"$/) || (value ~ /^'\''.*'\''$/)) { + value = substr(value, 2, length(value) - 2) + } + + print value + exit + } + ' "$file" +} + +version="$(read_key version)" +preview_tag="$(read_key preview-tag)" + +if [[ -z "$version" || ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "terrable_build must include version = X.Y.Z" >&2 + exit 1 +fi + +if [[ -n "$preview_tag" && ! "$preview_tag" =~ ^[0-9A-Za-z][0-9A-Za-z.-]*$ ]]; then + echo "terrable_build preview-tag must match ^[0-9A-Za-z][0-9A-Za-z.-]*$" >&2 + exit 1 +fi + +echo "version=$version" +echo "preview_tag=$preview_tag" diff --git a/terrable_build.go b/terrable_build.go index e4e325d..c3b5b67 100644 --- a/terrable_build.go +++ b/terrable_build.go @@ -5,6 +5,9 @@ import ( "strings" ) +// These are set by GoReleaser via ldflags on tagged and snapshot builds. +var version string + //go:embed terrable_build var configFile string @@ -24,10 +27,40 @@ func buildInfo() map[string]string { if len(parts) == 2 { key := strings.TrimSpace(parts[0]) - value := strings.TrimSpace(parts[1]) + value := normalizeBuildValue(parts[1]) config[key] = value } } return config } + +func buildVersion() string { + if version != "" { + return version + } + + info := buildInfo() + + if fileVersion := info["version"]; fileVersion != "" { + if previewTag := info["preview-tag"]; previewTag != "" { + return fileVersion + "-" + previewTag + } + + return fileVersion + } + + return "dev" +} + +func normalizeBuildValue(value string) string { + value = strings.TrimSpace(value) + + if len(value) >= 2 { + if (value[0] == '"' && value[len(value)-1] == '"') || (value[0] == '\'' && value[len(value)-1] == '\'') { + return value[1 : len(value)-1] + } + } + + return value +} diff --git a/terrable_build_test.go b/terrable_build_test.go new file mode 100644 index 0000000..72e6702 --- /dev/null +++ b/terrable_build_test.go @@ -0,0 +1,76 @@ +package main + +import "testing" + +func TestBuildInfoNormalizesQuotedValues(t *testing.T) { + originalConfig := configFile + originalVersion := version + t.Cleanup(func() { + configFile = originalConfig + version = originalVersion + }) + + configFile = ` +version = "1.2.3" +preview-tag = 'rc.1' +` + version = "" + + info := buildInfo() + + if got := info["version"]; got != "1.2.3" { + t.Fatalf("expected version 1.2.3, got %q", got) + } + + if got := info["preview-tag"]; got != "rc.1" { + t.Fatalf("expected preview-tag rc.1, got %q", got) + } +} + +func TestBuildVersionPrefersInjectedVersion(t *testing.T) { + originalConfig := configFile + originalVersion := version + t.Cleanup(func() { + configFile = originalConfig + version = originalVersion + }) + + configFile = "version = 1.2.3\npreview-tag = rc.1\n" + version = "9.9.9" + + if got := buildVersion(); got != "9.9.9" { + t.Fatalf("expected injected version, got %q", got) + } +} + +func TestBuildVersionUsesPreviewTagFromBuildInfo(t *testing.T) { + originalConfig := configFile + originalVersion := version + t.Cleanup(func() { + configFile = originalConfig + version = originalVersion + }) + + configFile = "version = 1.2.3\npreview-tag = preview.4\n" + version = "" + + if got := buildVersion(); got != "1.2.3-preview.4" { + t.Fatalf("expected preview build version, got %q", got) + } +} + +func TestBuildVersionDefaultsToDev(t *testing.T) { + originalConfig := configFile + originalVersion := version + t.Cleanup(func() { + configFile = originalConfig + version = originalVersion + }) + + configFile = "" + version = "" + + if got := buildVersion(); got != "dev" { + t.Fatalf("expected dev fallback, got %q", got) + } +}