Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 143 additions & 3 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,31 @@ on:
type: string
default: ""
description: "Optional release storage path segment. Defaults to the repository name."
go_main_package:
required: false
type: string
default: ""
description: "Optional relative Go main package. Defaults to ./cmd/<repository-name>."
brew_tap:
required: false
type: string
default: "homebrew-baton"
description: "ConductorOne Homebrew tap repository name."
go_source_hygiene:
required: false
type: boolean
default: false
description: "Run go generate and go mod tidy, then require a clean source tree."
go_vulnerability_scan:
required: false
type: boolean
default: false
description: "Run govulncheck for Linux, macOS, and Windows before release."
verify_module_version:
required: false
type: boolean
default: false
description: "Require generated binaries to embed the release tag as their module version."
lambda:
required: false
type: boolean
Expand Down Expand Up @@ -88,6 +113,7 @@ concurrency:
jobs:
validate-inputs:
runs-on: ubuntu-latest

steps:
- name: Validate tag format
env:
Expand Down Expand Up @@ -174,8 +200,11 @@ jobs:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
outputs:
ref: ${{ steps.workflow-version.outputs.sha }}
go_main_package: ${{ steps.release-options.outputs.go_main_package }}
brew_tap: ${{ steps.release-options.outputs.brew_tap }}
steps:
- name: Determine workflows ref
id: workflow-version
Expand All @@ -185,8 +214,95 @@ jobs:
file-name: "release.yaml"
github-token: ${{ secrets.GITHUB_TOKEN }}

goreleaser-binaries:
- name: Checkout pinned connector workflows
uses: actions/checkout@v5
with:
path: _workflows
repository: ConductorOne/github-workflows
ref: ${{ steps.workflow-version.outputs.sha }}
persist-credentials: false

- name: Normalize release options
id: release-options
working-directory: _workflows
shell: bash
env:
GO_MAIN_PACKAGE_INPUT: ${{ inputs.go_main_package }}
BREW_TAP_INPUT: ${{ inputs.brew_tap }}
REPOSITORY_NAME: ${{ github.event.repository.name }}
run: |
./scripts/normalize-release-options.sh \
"$REPOSITORY_NAME" \
"$GO_MAIN_PACKAGE_INPUT" \
"$BREW_TAP_INPUT" >> "$GITHUB_OUTPUT"

release-preflight:
needs: determine-workflows-ref
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout caller repo
if: inputs.go_source_hygiene == true || inputs.go_vulnerability_scan == true
uses: actions/checkout@v5
with:
path: _caller
repository: ${{ github.event.repository.full_name }}
ref: refs/tags/${{ inputs.tag }}
fetch-depth: 0
persist-credentials: false

- name: Verify caller checkout matches release tag
if: inputs.go_source_hygiene == true || inputs.go_vulnerability_scan == true
working-directory: _caller
shell: bash
env:
RELEASE_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
tag_commit="$(git rev-list -n 1 "refs/tags/$RELEASE_TAG")"
head_commit="$(git rev-parse HEAD)"
if [ "$head_commit" != "$tag_commit" ]; then
echo "::error::Checked out $head_commit but refs/tags/$RELEASE_TAG resolves to $tag_commit"
exit 1
fi

- name: Set up Go for caller
if: inputs.go_source_hygiene == true || inputs.go_vulnerability_scan == true
uses: actions/setup-go@v6
with:
go-version-file: "_caller/go.mod"
cache: false

- name: Verify generated source and module hygiene
if: inputs.go_source_hygiene == true
working-directory: _caller
shell: bash
run: |
set -euo pipefail
go generate ./...
go mod tidy
if [ -n "$(git status --porcelain)" ]; then
echo "::error::go generate or go mod tidy modified the release source tree"
git status --porcelain
exit 1
fi

- name: Check for known vulnerabilities
if: inputs.go_vulnerability_scan == true
working-directory: _caller
shell: bash
env:
GOTOOLCHAIN: auto
run: |
set -euo pipefail
go install golang.org/x/vuln/cmd/govulncheck@v1.7.0
for goos in linux darwin windows; do
GOOS="$goos" govulncheck ./...
done

goreleaser-binaries:
needs: [determine-workflows-ref, release-preflight]
runs-on: macos-latest
permissions:
contents: read
Expand Down Expand Up @@ -265,6 +381,8 @@ jobs:
working-directory: _workflows
env:
REPO_NAME: ${{ github.event.repository.name }}
GO_MAIN_PACKAGE: ${{ needs.determine-workflows-ref.outputs.go_main_package }}
BREW_TAP: ${{ needs.determine-workflows-ref.outputs.brew_tap }}
BREW_SKIP_UPLOAD: ${{ inputs.brew != true }}
# For provenance predicate template
WORKFLOWS_REF: ${{ needs.determine-workflows-ref.outputs.ref }}
Expand Down Expand Up @@ -310,6 +428,25 @@ jobs:
AC_PASSWORD: ${{ secrets.AC_PASSWORD }}
AC_PROVIDER: ${{ secrets.AC_PROVIDER }}

- name: Verify binary module version
if: inputs.verify_module_version == true
working-directory: _caller
shell: bash
env:
RELEASE_TAG: ${{ inputs.tag }}
REPOSITORY_NAME: ${{ github.event.repository.name }}
run: |
set -euo pipefail
binary="$(find dist -type f -name "$REPOSITORY_NAME" -print -quit)"
if [ -z "$binary" ]; then
echo "::error::No generated $REPOSITORY_NAME binary found for module version verification"
exit 1
fi
module_version="$(go version -m "$binary" | awk '$1 == "mod" { print $3; exit }')"
if [ "$module_version" != "$RELEASE_TAG" ]; then
echo "::error::Generated binary module version is '$module_version', expected '$RELEASE_TAG'"
exit 1
fi
- name: Generate SLSA provenance for archives
working-directory: _workflows
env:
Expand Down Expand Up @@ -481,7 +618,7 @@ jobs:

goreleaser-windows:
if: inputs.msi == true
needs: determine-workflows-ref
needs: [determine-workflows-ref, release-preflight]
runs-on: windows-latest
permissions:
contents: read
Expand Down Expand Up @@ -592,6 +729,7 @@ jobs:
shell: bash
env:
REPO_NAME: ${{ github.event.repository.name }}
GO_MAIN_PACKAGE: ${{ needs.determine-workflows-ref.outputs.go_main_package }}
WXS_PATH: ${{ steps.wxs.outputs.wxs_path }}
WORKFLOWS_REF: ${{ needs.determine-workflows-ref.outputs.ref }}
RELEASE_TAG: ${{ inputs.tag }}
Expand Down Expand Up @@ -765,7 +903,7 @@ jobs:

goreleaser-docker:
if: inputs.docker == true || inputs.lambda == true
needs: determine-workflows-ref
needs: [determine-workflows-ref, release-preflight]
permissions:
id-token: write
contents: read
Expand Down Expand Up @@ -840,6 +978,7 @@ jobs:
working-directory: _workflows
env:
REPO_NAME: ${{ github.event.repository.name }}
GO_MAIN_PACKAGE: ${{ needs.determine-workflows-ref.outputs.go_main_package }}
DOCKERFILE_PATH: ../_workflows/_generated/Dockerfile
DIST_DIR: dist/oci
PUBLIC_ECR_PUBLISH_TAG: release-candidate-${{ github.run_id }}-${{ github.run_attempt }}
Expand Down Expand Up @@ -898,6 +1037,7 @@ jobs:
working-directory: _workflows
env:
REPO_NAME: ${{ github.event.repository.name }}
GO_MAIN_PACKAGE: ${{ needs.determine-workflows-ref.outputs.go_main_package }}
DOCKERFILE_LAMBDA_PATH: ../_workflows/_generated/Dockerfile.lambda
DIST_DIR: dist/lambda
run: |
Expand Down
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ test-go:
.PHONY: test-scripts
test-scripts:
bash scripts/test-derive-iam-role-name.sh
bash scripts/test-normalize-release-options.sh
python3 scripts/test-release-config-templates.py
bash scripts/test-s3-release-uploads.sh
if command -v pwsh >/dev/null 2>&1; then pwsh -NoProfile -File scripts/test-s3-release-uploads.ps1; else echo "pwsh not found; skipping PowerShell S3 release upload tests"; fi

Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,18 @@ The release workflow accepts the following input parameters:
|-|-|-|-|
| `tag` | Yes | - | The release tag (must be valid semver with `v` prefix, e.g., `v1.0.0`) |
| `release_storage_name` | No | `""` | Optional S3 release path segment matching `^[a-z][a-z0-9-]{0,99}$`; defaults to the repository name |
| `go_main_package` | No | `""` | Relative Go main package. Defaults to `./cmd/<repository-name>`; use `./` for a root command package. |
| `brew_tap` | No | `homebrew-baton` | ConductorOne Homebrew tap repository name. Must not contain a path separator. |
| `go_source_hygiene` | No | `false` | Run `go generate ./...` and `go mod tidy`; fail if either changes the tagged source tree. |
| `go_vulnerability_scan` | No | `false` | Run `govulncheck` for Linux, macOS, and Windows before release. |
| `verify_module_version` | No | `false` | Require a generated binary to embed the release tag as its Go module version. |
| `lambda` | No | `true` | Whether to release with Lambda image support |
| `docker` | No | `true` | Whether to release with Docker image support |
| `dockerfile_template` | No | `""` | Path to a custom Dockerfile in your repo (only valid when `lambda: false`) |
| `docker_extra_files` | No | `""` | Comma-separated list of extra files/dirs to include in Docker build context |
| `msi` | No | `true` | Whether to build MSI Windows installers |
| `msi_wxs_path` | No | `""` | Path to custom WXS template for MSI installer (uses default if not set) |
| `brew` | No | `true` | Whether to publish a Homebrew formula to the public `conductorone/homebrew-baton` tap |
| `brew` | No | `true` | Whether to publish a Homebrew formula to the selected public `conductorone` tap |

2. Ensure your repository has the following secrets configured:

Expand Down
11 changes: 10 additions & 1 deletion docs/release-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,16 @@ Validates workflow inputs before proceeding:

### determine-workflows-ref

Resolves the exact SHA of the shared workflow being used. This pinned reference is embedded in all provenance attestations, ensuring verifiability.
Resolves the exact SHA of the shared workflow, then uses the pinned workflow source to normalize `go_main_package` and `brew_tap`. An omitted main package becomes `./cmd/<repository-name>`; a root package uses `./`. The workflow rejects non-relative package paths and tap values containing a path separator. The pinned reference is embedded in all provenance attestations, ensuring verifiability.

### release-preflight

Runs opted-in source integrity checks against the exact tagged caller source before any build job:

- `go_source_hygiene` runs `go generate ./...` and `go mod tidy`, then rejects a changed tree.
- `go_vulnerability_scan` runs `govulncheck` for Linux, macOS, and Windows.

`verify_module_version` checks a generated binary after GoReleaser and before provenance or artifact upload. It requires the embedded Go module version to equal the release tag.

### goreleaser-binaries (macOS)

Expand Down
32 changes: 32 additions & 0 deletions scripts/normalize-release-options.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail

if [ "$#" -ne 3 ]; then
echo "usage: $0 <repository-name> <go-main-package> <brew-tap>" >&2
exit 2
fi

repository_name="$1"
go_main_package="$2"
brew_tap="$3"

if [ -z "$go_main_package" ]; then
go_main_package="./cmd/${repository_name}"
fi
relative_package_pattern='^\./([A-Za-z0-9][A-Za-z0-9._-]*/)*[A-Za-z0-9][A-Za-z0-9._-]*$'
if [[ "$go_main_package" != "./" && ! "$go_main_package" =~ $relative_package_pattern ]]; then
echo "go_main_package must be ./ or a relative package path without empty, . or .. components: $go_main_package" >&2
exit 1
fi

if [ -z "$brew_tap" ]; then
brew_tap="homebrew-baton"
fi
brew_tap_pattern='^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$'
if [[ ! "$brew_tap" =~ $brew_tap_pattern ]]; then
echo "brew_tap must be a GitHub repository name without a path separator: $brew_tap" >&2
exit 1
fi

printf 'go_main_package=%s\n' "$go_main_package"
printf 'brew_tap=%s\n' "$brew_tap"
36 changes: 36 additions & 0 deletions scripts/test-normalize-release-options.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
set -euo pipefail

script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
script="${script_dir}/normalize-release-options.sh"

assert_output() {
local want="$1"
shift
local got
got="$(bash "$script" "$@")"
if [ "$got" != "$want" ]; then
echo "got: $got" >&2
echo "want: $want" >&2
exit 1
fi
}

assert_failure() {
if bash "$script" "$@" >/dev/null 2>&1; then
echo "expected failure: $script $*" >&2
exit 1
fi
}

assert_output $'go_main_package=./cmd/bridge-client\nbrew_tap=homebrew-baton' bridge-client "" ""
assert_output $'go_main_package=./\nbrew_tap=homebrew-cone' c1i ./ homebrew-cone
assert_output $'go_main_package=./cmd/release\nbrew_tap=homebrew-baton' c1i ./cmd/release homebrew-baton

assert_failure c1i /cmd/c1i homebrew-baton
assert_failure c1i ../cmd/c1i homebrew-baton
assert_failure c1i ./cmd/../c1i homebrew-baton
assert_failure c1i ./cmd//c1i homebrew-baton
assert_failure c1i ./cmd/ homebrew-baton
assert_failure c1i ./ owner/homebrew-cone
assert_failure c1i ./ 'homebrew cone'
Loading