Skip to content
Open
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
329 changes: 325 additions & 4 deletions .github/workflows/release.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ The release workflow accepts the following input parameters:
| `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 selected public `conductorone` tap |
| `brew` | No | `true` | Whether to publish a Homebrew formula to the selected public `conductorone` tap. The formula is macOS-only: the Linux tarballs are built in a parallel job, so their hashes are not available when the formula is rendered. |

2. Ensure your repository has the following secrets configured:

Expand Down
51 changes: 51 additions & 0 deletions cmd/merge-manifests/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,19 @@ const (
AttestationTypeInTotoV1 = "https://in-toto.io/Statement/v1"
// PredicateTypeSLSAProvenanceV1 is the SLSA v1 provenance predicate type
PredicateTypeSLSAProvenanceV1 = "https://slsa.dev/provenance/v1"
// ChecksumsAssetKey is the assets map key holding the unified checksums file
ChecksumsAssetKey = "checksums"
)

func main() {
var (
binariesManifest string
linuxManifest string
imagesManifest string
windowsManifest string
)
flag.StringVar(&binariesManifest, "binaries-manifest", "", "JSON string of binaries manifest")
flag.StringVar(&linuxManifest, "linux-manifest", "", "JSON string of Linux manifest")
flag.StringVar(&imagesManifest, "images-manifest", "", "JSON string of images manifest (optional)")
flag.StringVar(&windowsManifest, "windows-manifest", "", "JSON string of Windows assets manifest (optional)")
flag.Parse()
Expand All @@ -34,6 +38,15 @@ func main() {
os.Exit(1)
}

// The Linux job always runs and publish-release-manifest requires it to have
// succeeded, so an empty Linux manifest means the tarballs were silently lost
// rather than intentionally skipped. Fail instead of publishing a manifest
// that omits every Linux platform.
if linuxManifest == "" || linuxManifest == "{}" {
fmt.Fprintf(os.Stderr, "merge-manifests: error: linux-manifest is required\n")
os.Exit(1)
}

// Parse binaries manifest
manifest := &pb.Manifest{}
opts := protojson.UnmarshalOptions{
Expand Down Expand Up @@ -109,6 +122,44 @@ func main() {
fmt.Fprintln(os.Stderr, "ℹ️ No images to add to manifest (docker job may have been skipped if no Dockerfile)")
}

// Merge Linux assets. The Linux tarballs are built in a job parallel to the
// macOS one, so they arrive as their own manifest rather than in the same dist
// directory as the darwin archives.
{
linux := &pb.Manifest{}
if err := opts.Unmarshal([]byte(linuxManifest), linux); err != nil {
fmt.Fprintf(os.Stderr, "merge-manifests: ::error::Invalid JSON in linux_manifest output\n")
fmt.Fprintf(os.Stderr, "merge-manifests: Raw content:\n%s\n", linuxManifest)
fmt.Fprintf(os.Stderr, "merge-manifests: Error: %v\n", err)
os.Exit(1)
}

assets := manifest.GetAssets()
if assets == nil {
assets = make(map[string]*pb.Asset)
manifest.SetAssets(assets)
}

added := 0
for key, asset := range linux.GetAssets() {
// The Linux job generates a partial checksums file so generate-manifest
// can hash each tarball, but the authoritative checksums asset is the
// unified one publish-release-manifest builds and rewrites afterwards.
if key == ChecksumsAssetKey {
continue
}
assets[key] = asset
added++
}

if added == 0 {
fmt.Fprintf(os.Stderr, "merge-manifests: ::error::Linux manifest contained no platform assets\n")
os.Exit(1)
}

fmt.Fprintf(os.Stderr, "✅ Added %d Linux assets to manifest\n", added)
}

// Merge Windows assets if present
if windowsManifest != "" && windowsManifest != "{}" {
// Windows manifest format: { "windows-amd64": { "filename": "...", ... }, "windows-amd64-msi": { ... } }
Expand Down
7 changes: 6 additions & 1 deletion docs/diagrams/release-workflow.dot
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ digraph ReleaseWorkflow {

determine_ref [label="determine-workflows-ref\n• resolve workflow SHA", fillcolor="#f9fafb"];

binaries [label="goreleaser-binaries\n• Linux + macOS archives\n• gon codesign (macOS)\n• SBOMs, provenance\n• upload to S3", fillcolor="#ecfeff"];
binaries [label="goreleaser-binaries\n• macOS archives\n• gon codesign\n• SBOMs, provenance\n• creates GitHub Release\n• upload to S3", fillcolor="#ecfeff"];

linux [label="goreleaser-linux\n• Linux tarballs\n• SBOMs, provenance\n• upload to S3", fillcolor="#ecfeff"];

windows [label="goreleaser-windows\n• Windows zip + MSI\n• WiX Toolset\n• SBOMs, provenance\n• upload to S3", fillcolor="#ecfeff"];

Expand All @@ -40,12 +42,15 @@ digraph ReleaseWorkflow {
tag -> validate;
validate -> determine_ref;
determine_ref -> binaries;
determine_ref -> linux;
determine_ref -> windows;
determine_ref -> docker;
binaries -> record;
linux -> record;
windows -> record;
docker -> record;
binaries -> s3 [label="artifacts"];
linux -> s3 [label="artifacts"];
windows -> s3 [label="artifacts"];
docker -> ecr [label="push"];
record -> s3 [label="manifest"];
Expand Down
Binary file modified docs/diagrams/release-workflow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 27 additions & 5 deletions docs/release-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The `release.yaml` workflow handles the complete release process for connector r

When a tag is pushed to a connector repository, the shared release workflow:

1. Builds binaries for macOS and Linux (with Apple codesigning)
1. Builds macOS binaries (with Apple codesigning) and Linux binaries, in parallel jobs
2. Builds Windows zip and MSI installer (with WiX Toolset)
3. Builds multi-arch Docker images
4. Signs all artifacts with Sigstore (keyless)
Expand Down Expand Up @@ -45,16 +45,38 @@ Runs opted-in source integrity checks against the exact tagged caller source bef

### goreleaser-binaries (macOS)

Builds and signs binary archives for macOS and Linux:
Builds and signs the macOS archives. It runs on a macOS runner because gon needs the
Apple toolchain and the signing keychain; nothing else in the job requires a Mac.

- Cross-compiles for darwin/linux (amd64/arm64)
- Apple codesigning via gon (macOS only)
- Cross-compiles for darwin/amd64 and darwin/arm64
- Apple codesigning via gon
- Generates SBOMs using Syft
- Creates SLSA v1 provenance attestations
- Signs SBOMs as attestation bundles
- Uploads all artifacts to S3 with no-overwrite writes
- Creates the GitHub Release for the tag and publishes the Homebrew formula

**Outputs:** `*.zip` (macOS), `*.tar.gz` (Linux), `*.provenance.sigstore.json`, `*.sbom.sigstore.json`
**Outputs:** `*.zip` (macOS), `*.provenance.sigstore.json`, `*.sbom.sigstore.json`

### goreleaser-linux (Linux)

Builds and signs the Linux tarballs, concurrently with the macOS and Windows jobs.
Splitting these two targets off the macOS runner stops them from competing for its
cores, and the same work costs less on a Linux runner.

- Cross-compiles for linux/amd64 and linux/arm64
- Generates SBOMs using Syft
- Creates SLSA v1 provenance attestations
- Signs SBOMs as attestation bundles
- Uploads all artifacts to S3 with no-overwrite writes
- Runs with publishing disabled: the GitHub Release does not exist yet, so the
tarballs are staged as a run artifact and attached by `publish-release-manifest`

**Outputs:** `*.tar.gz` (Linux), `*.provenance.sigstore.json`, `*.sbom.sigstore.json`

**Homebrew:** the tap formula is rendered by GoReleaser in the macOS job, which no
longer has the Linux archive hashes. The formula is therefore macOS-only; Linux users
should install from the tarballs or use the container images.

### goreleaser-windows (Windows)

Expand Down
14 changes: 13 additions & 1 deletion scripts/test-release-config-templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
ROOT = Path(__file__).resolve().parent.parent
TEMPLATES = {
"binaries": ROOT / "templates/.goreleaser-binaries-template.yaml.tmpl",
"linux": ROOT / "templates/.goreleaser-linux-template.yaml.tmpl",
"windows": ROOT / "templates/.goreleaser-windows-template.yaml.tmpl",
"oci": ROOT / "templates/.goreleaser-docker-oci-template.yaml.tmpl",
"lambda": ROOT / "templates/.goreleaser-docker-lambda-template.yaml.tmpl",
Expand Down Expand Up @@ -56,12 +57,23 @@ def verify_case(go_main_package: str, brew_tap: str) -> None:
}
rendered = {name: render(path, values) for name, path in TEMPLATES.items()}

assert_main(rendered["binaries"], go_main_package, 3, "binaries template")
# binaries is darwin-only: the two linux targets live in the linux template.
assert_main(rendered["binaries"], go_main_package, 2, "binaries template")
assert_main(rendered["linux"], go_main_package, 1, "linux template")
assert_main(rendered["windows"], go_main_package, 1, "windows template")
assert_main(rendered["oci"], go_main_package, 1, "OCI template")
assert_main(rendered["lambda"], go_main_package, 1, "Lambda template")
assert_contains(rendered["binaries"], f'name: "{brew_tap}"', "binaries template")

# The macOS job must not rebuild the linux targets: that is the whole point of
# the split, and a stray linux build here would also produce duplicate archives.
if "- linux\n" in rendered["binaries"]:
raise AssertionError("binaries template still declares a linux target")
# Only the macOS job may create the GitHub Release, and only it publishes brew.
assert_contains(rendered["linux"], "release:\n disable: true", "linux template")
if "brews:" in rendered["linux"]:
raise AssertionError("linux template must not publish a Homebrew formula")


def main() -> int:
verify_case("./cmd/bridge-client", "homebrew-baton")
Expand Down
1 change: 1 addition & 0 deletions scripts/test-release-workflow-tag-pin.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ assert_tag_pin() {
}

assert_tag_pin goreleaser-binaries "Run GoReleaser"
assert_tag_pin goreleaser-linux "Run GoReleaser"
assert_tag_pin goreleaser-windows "Run GoReleaser for Windows"
assert_tag_pin goreleaser-docker "Run GoReleaser for Docker OCI"
assert_tag_pin goreleaser-docker "Run GoReleaser for Lambda"
37 changes: 10 additions & 27 deletions templates/.goreleaser-binaries-template.yaml.tmpl
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
## Binary template for signed artifacts, pushes to public registry (S3 bucket)
## macOS template for signed artifacts, pushes to public registry (S3 bucket)
## Runs on a macOS runner because gon needs the Apple toolchain and keychain.
## Linux tarballs are built by the goreleaser-linux job; Windows by goreleaser-windows.
## This config owns GitHub Release creation for the whole release.
version: 2
project_name: "${REPO_NAME}"
builds:
- binary: "${REPO_NAME}"
env:
- CGO_ENABLED=0
id: linux
main: "${GO_MAIN_PACKAGE}"
goos:
- linux
goarch:
- amd64
- arm64
# Note: Windows builds moved to dedicated goreleaser-windows job for MSI support
- binary: "${REPO_NAME}"
env:
- CGO_ENABLED=0
Expand All @@ -38,14 +30,6 @@ builds:
post:
- gon ../_workflows/_generated/.gon-arm64.json
archives:
- id: linux-archive
builds:
- linux
format: tar.gz
name_template: "{{ .ProjectName }}-v{{ .Version }}-{{ .Os }}-{{ .Arch }}"
files:
- none*
# Note: Windows archive moved to dedicated goreleaser-windows job
- id: darwin-archive
builds:
- macos-amd64
Expand All @@ -56,15 +40,13 @@ archives:
- none*
release:
ids:
- linux-archive
- darwin-archive
snapshot:
version_template: "{{ incpatch .Version }}-dev"
checksum:
# Note: checksums are NOT uploaded here - they're merged with Windows hashes
# and uploaded by the record-connector-registry job
# Note: checksums are NOT uploaded here - they're merged with the Linux and
# Windows hashes and uploaded by the publish-release-manifest job.
ids:
- linux-archive
- darwin-archive
sboms:
- artifacts: archive
Expand All @@ -74,7 +56,6 @@ signs:
cmd: cosign
artifacts: archive
ids:
- linux-archive
- darwin-archive
certificate: "{{ .Env.artifact }}.cert"
args:
Expand All @@ -85,9 +66,11 @@ signs:
- "{{ .Env.artifact }}"
env:
- COSIGN_EXPERIMENTAL=1
# Note: checksums signing moved to record-connector-registry job
# to allow merging with Windows hashes first
# Note: checksums signing moved to publish-release-manifest job
# to allow merging with the Linux and Windows hashes first
brews:
# macOS-only formula: the Linux tarballs are built in a parallel job, so their
# hashes are not available here. See the PR that split goreleaser-linux out.
- repository:
owner: conductorone
name: "${BREW_TAP}"
Expand Down
63 changes: 63 additions & 0 deletions templates/.goreleaser-linux-template.yaml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
## Linux template for signed tar.gz artifacts, pushes to public registry (S3 bucket)
## Split out of the macOS binaries config so the two linux targets stop consuming
## cores on the macOS runner, which is only needed for gon codesigning.
##
## GitHub Release creation stays with the macOS binaries job (it owns the release
## body and runs last), so this config never publishes. publish-release-manifest
## attaches these tarballs to the release once it exists.
version: 2
project_name: "${REPO_NAME}"
builds:
- binary: "${REPO_NAME}"
env:
- CGO_ENABLED=0
id: linux
main: "${GO_MAIN_PACKAGE}"
goos:
- linux
goarch:
- amd64
- arm64
archives:
- id: linux-archive
builds:
- linux
format: tar.gz
name_template: "{{ .ProjectName }}-v{{ .Version }}-{{ .Os }}-{{ .Arch }}"
files:
- none*
release:
disable: true
snapshot:
version_template: "{{ incpatch .Version }}-dev"
checksum:
# Generated so generate-manifest can read per-asset hashes and so the job can
# export them for merging. The file itself is never uploaded; the unified
# checksums file is built and signed by publish-release-manifest.
ids:
- linux-archive
sboms:
- artifacts: archive
signs:
- id: cosign-archives
output: true
cmd: cosign
artifacts: archive
ids:
- linux-archive
certificate: "{{ .Env.artifact }}.cert"
args:
- "sign-blob"
- "--yes"
- "--output-signature={{ .Env.signature }}"
- "--output-certificate={{ .Env.certificate }}"
- "{{ .Env.artifact }}"
env:
- COSIGN_EXPERIMENTAL=1
changelog:
filters:
exclude:
- "^docs:"
- typo
- lint
- Merge pull request