Skip to content

Commit bd3e154

Browse files
Parameterize shared Go release entrypoints (#111)
## Scope Add the release options and preflight controls needed to bring cone and c1i onto the shared trusted release path. ## Change - Normalize and validate go_main_package from the pinned github-workflows checkout, then use it in every GoReleaser target. - Add brew_tap with a fixed ConductorOne owner and a validated repository-name-only value. - Add opt-in source hygiene, multi-platform govulncheck, and post-build module-version checks. - Keep existing defaults unchanged for every caller. ## Security Inputs are never interpolated into shell commands. The normalizer rejects absolute, traversal, empty-component, and YAML-significant Go package paths, plus Homebrew tap path separators. Preflight checks run against the exact tag target before build; module-version verification runs before provenance or artifact upload. ## Verification - make test workflow-validate - make verify is blocked locally because buf is not installed. --------- Co-authored-by: Paul Querna <paul.querna@conductorone.com> Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
1 parent 4775fbf commit bd3e154

11 files changed

Lines changed: 318 additions & 13 deletions

.github/workflows/release.yaml

Lines changed: 143 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,31 @@ on:
2222
type: string
2323
default: ""
2424
description: "Optional release storage path segment. Defaults to the repository name."
25+
go_main_package:
26+
required: false
27+
type: string
28+
default: ""
29+
description: "Optional relative Go main package. Defaults to ./cmd/<repository-name>."
30+
brew_tap:
31+
required: false
32+
type: string
33+
default: "homebrew-baton"
34+
description: "ConductorOne Homebrew tap repository name."
35+
go_source_hygiene:
36+
required: false
37+
type: boolean
38+
default: false
39+
description: "Run go generate and go mod tidy, then require a clean source tree."
40+
go_vulnerability_scan:
41+
required: false
42+
type: boolean
43+
default: false
44+
description: "Run govulncheck for Linux, macOS, and Windows before release."
45+
verify_module_version:
46+
required: false
47+
type: boolean
48+
default: false
49+
description: "Require generated binaries to embed the release tag as their module version."
2550
lambda:
2651
required: false
2752
type: boolean
@@ -88,6 +113,7 @@ concurrency:
88113
jobs:
89114
validate-inputs:
90115
runs-on: ubuntu-latest
116+
91117
steps:
92118
- name: Validate tag format
93119
env:
@@ -174,8 +200,11 @@ jobs:
174200
runs-on: ubuntu-latest
175201
permissions:
176202
actions: read
203+
contents: read
177204
outputs:
178205
ref: ${{ steps.workflow-version.outputs.sha }}
206+
go_main_package: ${{ steps.release-options.outputs.go_main_package }}
207+
brew_tap: ${{ steps.release-options.outputs.brew_tap }}
179208
steps:
180209
- name: Determine workflows ref
181210
id: workflow-version
@@ -185,8 +214,95 @@ jobs:
185214
file-name: "release.yaml"
186215
github-token: ${{ secrets.GITHUB_TOKEN }}
187216

188-
goreleaser-binaries:
217+
- name: Checkout pinned connector workflows
218+
uses: actions/checkout@v5
219+
with:
220+
path: _workflows
221+
repository: ConductorOne/github-workflows
222+
ref: ${{ steps.workflow-version.outputs.sha }}
223+
persist-credentials: false
224+
225+
- name: Normalize release options
226+
id: release-options
227+
working-directory: _workflows
228+
shell: bash
229+
env:
230+
GO_MAIN_PACKAGE_INPUT: ${{ inputs.go_main_package }}
231+
BREW_TAP_INPUT: ${{ inputs.brew_tap }}
232+
REPOSITORY_NAME: ${{ github.event.repository.name }}
233+
run: |
234+
./scripts/normalize-release-options.sh \
235+
"$REPOSITORY_NAME" \
236+
"$GO_MAIN_PACKAGE_INPUT" \
237+
"$BREW_TAP_INPUT" >> "$GITHUB_OUTPUT"
238+
239+
release-preflight:
189240
needs: determine-workflows-ref
241+
runs-on: ubuntu-latest
242+
permissions:
243+
contents: read
244+
steps:
245+
- name: Checkout caller repo
246+
if: inputs.go_source_hygiene == true || inputs.go_vulnerability_scan == true
247+
uses: actions/checkout@v5
248+
with:
249+
path: _caller
250+
repository: ${{ github.event.repository.full_name }}
251+
ref: refs/tags/${{ inputs.tag }}
252+
fetch-depth: 0
253+
persist-credentials: false
254+
255+
- name: Verify caller checkout matches release tag
256+
if: inputs.go_source_hygiene == true || inputs.go_vulnerability_scan == true
257+
working-directory: _caller
258+
shell: bash
259+
env:
260+
RELEASE_TAG: ${{ inputs.tag }}
261+
run: |
262+
set -euo pipefail
263+
tag_commit="$(git rev-list -n 1 "refs/tags/$RELEASE_TAG")"
264+
head_commit="$(git rev-parse HEAD)"
265+
if [ "$head_commit" != "$tag_commit" ]; then
266+
echo "::error::Checked out $head_commit but refs/tags/$RELEASE_TAG resolves to $tag_commit"
267+
exit 1
268+
fi
269+
270+
- name: Set up Go for caller
271+
if: inputs.go_source_hygiene == true || inputs.go_vulnerability_scan == true
272+
uses: actions/setup-go@v6
273+
with:
274+
go-version-file: "_caller/go.mod"
275+
cache: false
276+
277+
- name: Verify generated source and module hygiene
278+
if: inputs.go_source_hygiene == true
279+
working-directory: _caller
280+
shell: bash
281+
run: |
282+
set -euo pipefail
283+
go generate ./...
284+
go mod tidy
285+
if [ -n "$(git status --porcelain)" ]; then
286+
echo "::error::go generate or go mod tidy modified the release source tree"
287+
git status --porcelain
288+
exit 1
289+
fi
290+
291+
- name: Check for known vulnerabilities
292+
if: inputs.go_vulnerability_scan == true
293+
working-directory: _caller
294+
shell: bash
295+
env:
296+
GOTOOLCHAIN: auto
297+
run: |
298+
set -euo pipefail
299+
go install golang.org/x/vuln/cmd/govulncheck@v1.7.0
300+
for goos in linux darwin windows; do
301+
GOOS="$goos" govulncheck ./...
302+
done
303+
304+
goreleaser-binaries:
305+
needs: [determine-workflows-ref, release-preflight]
190306
runs-on: macos-latest
191307
permissions:
192308
contents: read
@@ -265,6 +381,8 @@ jobs:
265381
working-directory: _workflows
266382
env:
267383
REPO_NAME: ${{ github.event.repository.name }}
384+
GO_MAIN_PACKAGE: ${{ needs.determine-workflows-ref.outputs.go_main_package }}
385+
BREW_TAP: ${{ needs.determine-workflows-ref.outputs.brew_tap }}
268386
BREW_SKIP_UPLOAD: ${{ inputs.brew != true }}
269387
# For provenance predicate template
270388
WORKFLOWS_REF: ${{ needs.determine-workflows-ref.outputs.ref }}
@@ -310,6 +428,25 @@ jobs:
310428
AC_PASSWORD: ${{ secrets.AC_PASSWORD }}
311429
AC_PROVIDER: ${{ secrets.AC_PROVIDER }}
312430

431+
- name: Verify binary module version
432+
if: inputs.verify_module_version == true
433+
working-directory: _caller
434+
shell: bash
435+
env:
436+
RELEASE_TAG: ${{ inputs.tag }}
437+
REPOSITORY_NAME: ${{ github.event.repository.name }}
438+
run: |
439+
set -euo pipefail
440+
binary="$(find dist -type f -name "$REPOSITORY_NAME" -print -quit)"
441+
if [ -z "$binary" ]; then
442+
echo "::error::No generated $REPOSITORY_NAME binary found for module version verification"
443+
exit 1
444+
fi
445+
module_version="$(go version -m "$binary" | awk '$1 == "mod" { print $3; exit }')"
446+
if [ "$module_version" != "$RELEASE_TAG" ]; then
447+
echo "::error::Generated binary module version is '$module_version', expected '$RELEASE_TAG'"
448+
exit 1
449+
fi
313450
- name: Generate SLSA provenance for archives
314451
working-directory: _workflows
315452
env:
@@ -481,7 +618,7 @@ jobs:
481618
482619
goreleaser-windows:
483620
if: inputs.msi == true
484-
needs: determine-workflows-ref
621+
needs: [determine-workflows-ref, release-preflight]
485622
runs-on: windows-latest
486623
permissions:
487624
contents: read
@@ -592,6 +729,7 @@ jobs:
592729
shell: bash
593730
env:
594731
REPO_NAME: ${{ github.event.repository.name }}
732+
GO_MAIN_PACKAGE: ${{ needs.determine-workflows-ref.outputs.go_main_package }}
595733
WXS_PATH: ${{ steps.wxs.outputs.wxs_path }}
596734
WORKFLOWS_REF: ${{ needs.determine-workflows-ref.outputs.ref }}
597735
RELEASE_TAG: ${{ inputs.tag }}
@@ -765,7 +903,7 @@ jobs:
765903
766904
goreleaser-docker:
767905
if: inputs.docker == true || inputs.lambda == true
768-
needs: determine-workflows-ref
906+
needs: [determine-workflows-ref, release-preflight]
769907
permissions:
770908
id-token: write
771909
contents: read
@@ -840,6 +978,7 @@ jobs:
840978
working-directory: _workflows
841979
env:
842980
REPO_NAME: ${{ github.event.repository.name }}
981+
GO_MAIN_PACKAGE: ${{ needs.determine-workflows-ref.outputs.go_main_package }}
843982
DOCKERFILE_PATH: ../_workflows/_generated/Dockerfile
844983
DIST_DIR: dist/oci
845984
PUBLIC_ECR_PUBLISH_TAG: release-candidate-${{ github.run_id }}-${{ github.run_attempt }}
@@ -898,6 +1037,7 @@ jobs:
8981037
working-directory: _workflows
8991038
env:
9001039
REPO_NAME: ${{ github.event.repository.name }}
1040+
GO_MAIN_PACKAGE: ${{ needs.determine-workflows-ref.outputs.go_main_package }}
9011041
DOCKERFILE_LAMBDA_PATH: ../_workflows/_generated/Dockerfile.lambda
9021042
DIST_DIR: dist/lambda
9031043
run: |

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ test-go:
1919
.PHONY: test-scripts
2020
test-scripts:
2121
bash scripts/test-derive-iam-role-name.sh
22+
bash scripts/test-normalize-release-options.sh
23+
python3 scripts/test-release-config-templates.py
2224
bash scripts/test-s3-release-uploads.sh
2325
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
2426

README.md

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

8792
2. Ensure your repository has the following secrets configured:
8893

docs/release-workflow.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,16 @@ Validates workflow inputs before proceeding:
3232

3333
### determine-workflows-ref
3434

35-
Resolves the exact SHA of the shared workflow being used. This pinned reference is embedded in all provenance attestations, ensuring verifiability.
35+
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.
36+
37+
### release-preflight
38+
39+
Runs opted-in source integrity checks against the exact tagged caller source before any build job:
40+
41+
- `go_source_hygiene` runs `go generate ./...` and `go mod tidy`, then rejects a changed tree.
42+
- `go_vulnerability_scan` runs `govulncheck` for Linux, macOS, and Windows.
43+
44+
`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.
3645

3746
### goreleaser-binaries (macOS)
3847

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
if [ "$#" -ne 3 ]; then
5+
echo "usage: $0 <repository-name> <go-main-package> <brew-tap>" >&2
6+
exit 2
7+
fi
8+
9+
repository_name="$1"
10+
go_main_package="$2"
11+
brew_tap="$3"
12+
13+
if [ -z "$go_main_package" ]; then
14+
go_main_package="./cmd/${repository_name}"
15+
fi
16+
relative_package_pattern='^\./([A-Za-z0-9][A-Za-z0-9._-]*/)*[A-Za-z0-9][A-Za-z0-9._-]*$'
17+
if [[ "$go_main_package" != "./" && ! "$go_main_package" =~ $relative_package_pattern ]]; then
18+
echo "go_main_package must be ./ or a relative package path without empty, . or .. components: $go_main_package" >&2
19+
exit 1
20+
fi
21+
22+
if [ -z "$brew_tap" ]; then
23+
brew_tap="homebrew-baton"
24+
fi
25+
brew_tap_pattern='^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$'
26+
if [[ ! "$brew_tap" =~ $brew_tap_pattern ]]; then
27+
echo "brew_tap must be a GitHub repository name without a path separator: $brew_tap" >&2
28+
exit 1
29+
fi
30+
31+
printf 'go_main_package=%s\n' "$go_main_package"
32+
printf 'brew_tap=%s\n' "$brew_tap"
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
script="${script_dir}/normalize-release-options.sh"
6+
7+
assert_output() {
8+
local want="$1"
9+
shift
10+
local got
11+
got="$(bash "$script" "$@")"
12+
if [ "$got" != "$want" ]; then
13+
echo "got: $got" >&2
14+
echo "want: $want" >&2
15+
exit 1
16+
fi
17+
}
18+
19+
assert_failure() {
20+
if bash "$script" "$@" >/dev/null 2>&1; then
21+
echo "expected failure: $script $*" >&2
22+
exit 1
23+
fi
24+
}
25+
26+
assert_output $'go_main_package=./cmd/bridge-client\nbrew_tap=homebrew-baton' bridge-client "" ""
27+
assert_output $'go_main_package=./\nbrew_tap=homebrew-cone' c1i ./ homebrew-cone
28+
assert_output $'go_main_package=./cmd/release\nbrew_tap=homebrew-baton' c1i ./cmd/release homebrew-baton
29+
30+
assert_failure c1i /cmd/c1i homebrew-baton
31+
assert_failure c1i ../cmd/c1i homebrew-baton
32+
assert_failure c1i ./cmd/../c1i homebrew-baton
33+
assert_failure c1i ./cmd//c1i homebrew-baton
34+
assert_failure c1i ./cmd/ homebrew-baton
35+
assert_failure c1i ./ owner/homebrew-cone
36+
assert_failure c1i ./ 'homebrew cone'

0 commit comments

Comments
 (0)