diff --git a/.github/workflows/codeowners.yml b/.github/workflows/codeowners.yml index f5d3e6d..031e078 100644 --- a/.github/workflows/codeowners.yml +++ b/.github/workflows/codeowners.yml @@ -25,11 +25,6 @@ jobs: with: fetch-depth: 0 - - name: 'Update action.yml to build locally' - run: | - sed -i "s/image: .*/image: 'Dockerfile'/" action.yml - cat action.yml - - name: 'Codeowners Plus' id: codeowners-plus uses: ./ diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 2deb2ed..5762692 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -21,7 +21,7 @@ jobs: - name: 'Set up Go' uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 with: - go-version: '1.26' + go-version-file: go.mod - name: 'Build' run: go build -v ./... @@ -37,7 +37,7 @@ jobs: - name: 'Set up Go' uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 with: - go-version: '1.26' + go-version-file: go.mod - name: 'Golangci-lint' uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index 0c943bf..6e191d3 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -2,19 +2,15 @@ name: goreleaser on: release: - types: [draft, published] + types: [draft] permissions: {} -env: - DOCKER_BUILDKIT: 1 - jobs: goreleaser: runs-on: ubuntu-latest permissions: contents: write - packages: write steps: - name: Checkout @@ -25,7 +21,7 @@ jobs: - name: Set up Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 with: - go-version: 1.26 + go-version-file: go.mod - name: Run GoReleaser uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 8452156..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: 'Publish to GHCR' - -on: - release: - types: [published] - -permissions: {} - -jobs: - update: - runs-on: ubuntu-latest - permissions: - contents: write - packages: write - steps: - - name: 'Checkout Code Repository' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: 'Get release version' - id: get_version - run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 - - - name: 'Build & Publish' - uses: elgohr/Publish-Docker-Github-Action@1c2f28ccd9476e8a936ac9a1f287405504c93304 # v5 - with: - name: multimediallc/codeowners-plus - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - registry: ghcr.io - tags: "latest,${{ steps.get_version.outputs.RELEASE_VERSION }}" - platforms: linux/amd64,linux/arm64 diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index de7a1be..0000000 --- a/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -FROM golang:1.26@sha256:313faae491b410a35402c05d35e7518ae99103d957308e940e1ae2cfa0aac29b AS builder - -WORKDIR /app - -COPY go.mod go.sum ./ -RUN go mod download - -COPY main.go . -COPY pkg ./pkg/ -COPY internal ./internal/ - -# Statically comple with CGO enabled will be needed if we integrate go-tree-sitter -# RUN GOOS=linux go build --ldflags '-extldflags "-static"' -v -o codeowners main.go - -# Statically compile with CGO disabled -RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -v -o codeowners main.go - -FROM alpine:latest@sha256:5b10f432ef3da1b8d4c7eb6c487f2f5a8f096bc91145e68878dd4a5019afde11 - -RUN apk update && apk add git - -COPY --from=builder /app/codeowners /codeowners -COPY entrypoint.sh /entrypoint.sh - -ENTRYPOINT ["/entrypoint.sh"] diff --git a/action.yml b/action.yml index 6f3dc3a..91e9d39 100644 --- a/action.yml +++ b/action.yml @@ -27,7 +27,88 @@ inputs: outputs: data: description: 'JSON string containing all the codeowners data (success, message, file-owners, file-optional, still-required)' + value: ${{ steps.run.outputs.data }} runs: - using: 'docker' - image: 'docker://ghcr.io/multimediallc/codeowners-plus:latest' + using: 'composite' + steps: + - name: 'Resolve codeowners-plus binary' + id: resolve + shell: bash + env: + # The release tag this commit belongs to. Non-empty only in release + # commits: set by scripts/prepare-release.sh and cleared by + # scripts/post-release.sh. When set, the action downloads that + # release's prebuilt binary; when empty (any non-release ref) it + # builds from the checked-out source. + RELEASE_VERSION: '' + run: | + set -euo pipefail + { + echo "release-version=${RELEASE_VERSION}" + echo "bin=${RUNNER_TEMP:-/tmp}/codeowners-plus-action/codeowners-plus" + } >>"$GITHUB_OUTPUT" + + # RELEASE_VERSION not set -> not a release: build from source (cached). + - name: 'Restore cached built binary' + id: buildcache + if: steps.resolve.outputs.release-version == '' + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ${{ steps.resolve.outputs.bin }} + key: codeowners-plus-build-${{ github.action_ref || github.sha }}-${{ runner.os }}-${{ runner.arch }} + + - name: 'Set up Go' + if: steps.resolve.outputs.release-version == '' && steps.buildcache.outputs.cache-hit != 'true' + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 + with: + go-version-file: ${{ github.action_path }}/go.mod + cache-dependency-path: ${{ github.action_path }}/go.sum + + - name: 'Build codeowners-plus from source' + if: steps.resolve.outputs.release-version == '' && steps.buildcache.outputs.cache-hit != 'true' + shell: bash + env: + ACTION_PATH: ${{ github.action_path }} + BIN: ${{ steps.resolve.outputs.bin }} + run: | + set -euo pipefail + mkdir -p "$(dirname "${BIN}")" + cd "${ACTION_PATH}" + CGO_ENABLED=0 \ + go build -trimpath -buildvcs=false -ldflags="-s -w" -o "${BIN}" . + + # RELEASE_VERSION set -> a release: download + verify the prebuilt binary (cached). + - name: 'Restore cached release binary' + id: bincache + if: steps.resolve.outputs.release-version != '' + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ${{ steps.resolve.outputs.bin }} + key: codeowners-plus-action-${{ steps.resolve.outputs.release-version }}-${{ runner.os }}-${{ runner.arch }} + + - name: 'Download codeowners-plus release binary' + if: steps.resolve.outputs.release-version != '' && steps.bincache.outputs.cache-hit != 'true' + shell: bash + env: + REPO: ${{ github.action_repository }} + ACTION_PATH: ${{ github.action_path }} + TAG: ${{ steps.resolve.outputs.release-version }} + BIN: ${{ steps.resolve.outputs.bin }} + run: '"${ACTION_PATH}/scripts/install-action.sh"' + + - name: 'Run codeowners-plus' + id: run + shell: bash + env: + # The hyphenated INPUT_GITHUB-TOKEN name is intentional: it mirrors + # what Docker actions export and is exactly what main.go reads via + # os.LookupEnv. Bash passes non-identifier env vars through to child + # processes untouched. + INPUT_GITHUB-TOKEN: ${{ inputs.github-token }} + INPUT_PR: ${{ inputs.pr }} + INPUT_REPOSITORY: ${{ inputs.repository }} + INPUT_VERBOSE: ${{ inputs.verbose }} + INPUT_QUIET: ${{ inputs.quiet }} + BIN: ${{ steps.resolve.outputs.bin }} + run: '"${BIN}"' diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100755 index 06279a1..0000000 --- a/entrypoint.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -l - -set -e - -git config --global --add safe.directory /github/workspace -git branch - -/codeowners diff --git a/go.mod b/go.mod index 734e3cd..1d86f6d 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module github.com/multimediallc/codeowners-plus go 1.25.0 +toolchain go1.26.4 + require ( github.com/bmatcuk/doublestar/v4 v4.10.0 github.com/boyter/gocodewalker v1.5.1 diff --git a/goreleaser.yml b/goreleaser.yml index c8bee33..24d9a5f 100644 --- a/goreleaser.yml +++ b/goreleaser.yml @@ -23,8 +23,25 @@ builds: goarch: - amd64 - arm64 + # The binary the GitHub Action downloads and runs (root main.go). + - id: "action" + main: . + binary: codeowners-plus-action + env: + - CGO_ENABLED=0 + ldflags: + - -s + - -w + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 archives: - - name_template: >- + - id: "cli" + ids: ["cli"] + name_template: >- {{ .ProjectName }}_{{ .Version }}_{{ title .Os }}_ {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 @@ -32,6 +49,11 @@ archives: {{- if .Arm }}v{{ .Arm }}{{ end -}} files: - none* + - id: "action" + ids: ["action"] + name_template: "codeowners-plus-action_{{ .Os }}_{{ .Arch }}" + files: + - none* checksum: name_template: 'checksums.txt' snapshot: diff --git a/scripts/install-action.sh b/scripts/install-action.sh new file mode 100755 index 0000000..0b34071 --- /dev/null +++ b/scripts/install-action.sh @@ -0,0 +1,95 @@ +#! /usr/bin/env bash +# Installs the prebuilt codeowners-plus binary for the current platform, +# verified against the release's checksums.txt. +# +# Local use (all env vars optional): +# scripts/install-action.sh # latest release -> ./codeowners-plus +# VERSION=v1.9.1 scripts/install-action.sh # a specific release +# BIN=/usr/local/bin/codeowners-plus scripts/install-action.sh +# curl -fsSL https://raw.githubusercontent.com/multimediallc/codeowners-plus/main/scripts/install-action.sh | bash +# +# Overrides: REPO, VERSION (or TAG), OS, ARCH, BIN. action.yml passes +# REPO/TAG/BIN; OS and ARCH are detected here so the script is self-contained. + +set -eu + +REPO="${REPO:-multimediallc/codeowners-plus}" +BIN="${BIN:-./codeowners-plus}" +TAG="${TAG:-${VERSION:-}}" + +# Detect OS unless overridden. Tokens match goreleaser's {{ .Os }}. +OS="${OS:-}" +if [ -z "${OS}" ]; then + case "$(uname -s)" in + Linux) OS="linux" ;; + Darwin) OS="darwin" ;; + *) + echo "Error: unsupported OS '$(uname -s)' (supported: linux, darwin)." >&2 + exit 1 + ;; + esac +fi + +# Detect ARCH unless overridden. Tokens match goreleaser's {{ .Arch }}. +ARCH="${ARCH:-}" +if [ -z "${ARCH}" ]; then + case "$(uname -m)" in + x86_64 | amd64) ARCH="amd64" ;; + arm64 | aarch64) ARCH="arm64" ;; + *) + echo "Error: unsupported arch '$(uname -m)' (supported: amd64, arm64)." >&2 + exit 1 + ;; + esac +fi + +# Default to the latest release when no version was requested. +if [ -z "${TAG}" ]; then + TAG="$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \ + | awk -F'"' '/"tag_name":/ {print $4; exit}')" + if [ -z "${TAG}" ]; then + echo "Error: could not determine the latest release of ${REPO}." >&2 + exit 1 + fi +fi + +asset="codeowners-plus-action_${OS}_${ARCH}.tar.gz" +binname="codeowners-plus-action" +base="https://github.com/${REPO}/releases/download/${TAG}" +tmp="$(mktemp -d)" +trap 'rm -rf "${tmp}"' EXIT + +echo "Downloading ${asset} from ${REPO} release ${TAG}" >&2 +curl -fsSL --retry 3 -o "${tmp}/${asset}" "${base}/${asset}" +curl -fsSL --retry 3 -o "${tmp}/checksums.txt" "${base}/checksums.txt" + +echo "Verifying ${asset} against checksums.txt" >&2 +expected="$(awk -v a="${asset}" '$2 == a {print $1}' "${tmp}/checksums.txt")" +if [ -z "${expected}" ]; then + echo "Error: ${asset} not found in checksums.txt" >&2 + exit 1 +fi +# Guard against a malformed digest: ' -c' treats an improperly +# formatted line as a skipped (passing) entry rather than a failure. +if ! printf '%s' "${expected}" | grep -Eq '^[0-9a-f]{64}$'; then + echo "Error: invalid checksum for ${asset} in checksums.txt" >&2 + exit 1 +fi +# sha256sum is GNU coreutils (Linux); macOS only ships shasum. +if command -v sha256sum >/dev/null 2>&1; then + verify=(sha256sum -c -) +else + verify=(shasum -a 256 -c -) +fi +if ! echo "${expected} ${tmp}/${asset}" | "${verify[@]}"; then + echo "Error: downloaded ${asset} does not match its release checksum" >&2 + exit 1 +fi + +echo "Extracting ${binname} from ${asset}" >&2 +tar -xzf "${tmp}/${asset}" -C "${tmp}" "${binname}" + +mkdir -p "$(dirname "${BIN}")" +mv "${tmp}/${binname}" "${BIN}" +chmod +x "${BIN}" +echo "Installed codeowners-plus ${TAG} to ${BIN}" >&2 diff --git a/scripts/post-release.sh b/scripts/post-release.sh index ec65034..9061f37 100755 --- a/scripts/post-release.sh +++ b/scripts/post-release.sh @@ -1,8 +1,8 @@ #! /usr/bin/env bash -set -e -set -u +set -eu +ACTIONS_FILE="action.yml" CLI_TOOL_FILE="tools/cli/main.go" README_FILE="README.md" @@ -39,23 +39,25 @@ else git checkout -b "${BRANCH_NAME}" fi -echo "Updating ${CLI_TOOL_FILE} and ${README_FILE}..." +echo "Updating ${ACTIONS_FILE}, ${CLI_TOOL_FILE}, and ${README_FILE}..." # sed -i works differently on macOS and Linux. # For GNU sed (Linux), -i without an argument is fine. # For BSD sed (macOS), -i requires an argument (even if empty string for no backup). if sed --version 2>/dev/null | grep -q GNU; then # GNU sed + sed -i "s|RELEASE_VERSION: '.*'|RELEASE_VERSION: ''|g" "${ACTIONS_FILE}" sed -i "s|Version: .*|Version: \"${DEV_TAG}\",|g" "${CLI_TOOL_FILE}" sed -i "s|codeowners-plus@.*|codeowners-plus@${VERSION_TAG}|g" "${README_FILE}" else # BSD sed (macOS) + sed -i '' "s|RELEASE_VERSION: '.*'|RELEASE_VERSION: ''|g" "${ACTIONS_FILE}" sed -i '' "s|Version: .*|Version: \"${DEV_TAG}\",|g" "${CLI_TOOL_FILE}" sed -i '' "s|codeowners-plus@.*|codeowners-plus@${VERSION_TAG}|g" "${README_FILE}" fi gofmt -w tools/cli -echo "${CLI_TOOL_FILE} and ${README_FILE} updated." +echo "${ACTIONS_FILE}, ${CLI_TOOL_FILE}, and ${README_FILE} updated." echo "Committing changes..." -git add "${CLI_TOOL_FILE}" "${README_FILE}" +git add "${ACTIONS_FILE}" "${CLI_TOOL_FILE}" "${README_FILE}" git commit -m "${VERSION_TAG}" echo "--- Post release process completed successfully! ---" diff --git a/scripts/prepare-release.sh b/scripts/prepare-release.sh index acf77cc..953e78d 100755 --- a/scripts/prepare-release.sh +++ b/scripts/prepare-release.sh @@ -1,7 +1,6 @@ #! /usr/bin/env bash -set -e -set -u +set -eu ACTIONS_FILE="action.yml" CLI_TOOL_FILE="tools/cli/main.go" @@ -73,11 +72,11 @@ echo "Updating ${ACTIONS_FILE}, ${CLI_TOOL_FILE}, and ${README_FILE} to replace # For GNU sed (Linux), -i without an argument is fine. # For BSD sed (macOS), -i requires an argument (even if empty string for no backup). if sed --version 2>/dev/null | grep -q GNU; then # GNU sed - sed -i "s|codeowners-plus:.*'|codeowners-plus:${VERSION_TAG}'|g" "${ACTIONS_FILE}" + sed -i "s|RELEASE_VERSION: '.*'|RELEASE_VERSION: '${VERSION_TAG}'|g" "${ACTIONS_FILE}" sed -i "s|Version: .*|Version: \"${VERSION_TAG}\",|g" "${CLI_TOOL_FILE}" sed -i "s|codeowners-plus@.*|codeowners-plus@${VERSION_TAG}|g" "${README_FILE}" else # BSD sed (macOS) - sed -i '' "s|codeowners-plus:.*'|codeowners-plus:${VERSION_TAG}'|g" "${ACTIONS_FILE}" + sed -i '' "s|RELEASE_VERSION: '.*'|RELEASE_VERSION: '${VERSION_TAG}'|g" "${ACTIONS_FILE}" sed -i '' "s|Version: .*|Version: \"${VERSION_TAG}\",|g" "${CLI_TOOL_FILE}" sed -i '' "s|codeowners-plus@.*|codeowners-plus@${VERSION_TAG}|g" "${README_FILE}" fi