diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 73b53d9..15919f1 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -617,6 +617,287 @@ jobs: echo "${DELIM}" } >> "$GITHUB_OUTPUT" + # ================================================================ + # Linux tarballs. Split out of goreleaser-binaries so the macOS + # runner only compiles the two darwin targets it actually needs a + # Mac for. Runs concurrently with the macOS and Windows jobs. + # ================================================================ + goreleaser-linux: + needs: [determine-workflows-ref, release-preflight] + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # <-- needed for cosign keyless (OIDC) + outputs: + linux_manifest: ${{ steps.generate-linux-manifest.outputs.linux_manifest }} + linux_checksums: ${{ steps.output-checksums.outputs.checksums }} + steps: + - name: Checkout caller repo + 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 + 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 + echo "Verified $RELEASE_TAG at $head_commit" + + - name: Checkout connector workflows + uses: actions/checkout@v5 + with: + path: _workflows + repository: ConductorOne/github-workflows + ref: ${{ needs.determine-workflows-ref.outputs.ref }} + persist-credentials: false + + - name: Derive AWS role names + id: role-names + working-directory: _workflows + shell: bash + env: + REPO_OWNER: ${{ github.event.repository.owner.login }} + REPO_NAME: ${{ github.event.repository.name }} + run: | + bash ./scripts/derive-iam-role-name.sh \ + --prefix GHA-Artifacts- \ + --suffix "${REPO_OWNER}-${REPO_NAME}" \ + --output-name gha_artifacts_role_name >> "$GITHUB_OUTPUT" + + - name: Set up Go for caller + uses: actions/setup-go@v6 + with: + go-version-file: "_caller/go.mod" + cache: false + + - name: Calculate S3 directory + id: s3-directory + shell: bash + run: | + ORG="${{ github.event.repository.owner.login }}" + REPO="${{ github.event.repository.name }}" + STORAGE_NAME="${{ inputs.release_storage_name }}" + TAG="${{ inputs.tag }}" + if [ -z "$STORAGE_NAME" ]; then + STORAGE_NAME="$REPO" + fi + echo "S3_DIRECTORY=releases/$ORG/$STORAGE_NAME/$TAG" >> "$GITHUB_OUTPUT" + + - name: Generate configs for Linux + working-directory: _workflows + env: + REPO_NAME: ${{ github.event.repository.name }} + GO_MAIN_PACKAGE: ${{ needs.determine-workflows-ref.outputs.go_main_package }} + # For provenance predicate template + WORKFLOWS_REF: ${{ needs.determine-workflows-ref.outputs.ref }} + RELEASE_TAG: ${{ inputs.tag }} + run: | + mkdir -p "${GENERATED_DIR}" + export BUILD_STARTED_ON=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + + envsubst < templates/.goreleaser-linux-template.yaml.tmpl | tee "${GENERATED_DIR}/.goreleaser.linux.yaml" + envsubst < templates/.slsa-provenance-predicate-template.json.tmpl | tee "${GENERATED_DIR}/predicate.json" + + - name: Install cosign + uses: sigstore/cosign-installer@v3 + + - name: Download syft + uses: anchore/sbom-action/download-syft@v0 + + - name: Configure AWS credentials via OIDC + uses: aws-actions/configure-aws-credentials@v5 + with: + role-to-assume: arn:aws:iam::025044153841:role/${{ steps.role-names.outputs.gha_artifacts_role_name }} + aws-region: us-west-2 + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + workdir: _caller + version: "~> v2.13" + args: release --clean --skip=publish --config ../_workflows/_generated/.goreleaser.linux.yaml + env: + GITHUB_TOKEN: ${{ secrets.RELENG_GITHUB_TOKEN }} + GORELEASER_CURRENT_TAG: ${{ inputs.tag }} + + - 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: + CALLER_DIST: ../_caller/dist + shell: bash + run: | + set -euo pipefail + + PROVENANCE_COUNT=0 + + for artifact in "${CALLER_DIST}"/*.tar.gz; do + [ -f "$artifact" ] || continue + [[ "$artifact" == *checksums* ]] && continue + + BASENAME=$(basename "$artifact") + echo "Generating provenance for: $BASENAME" + cosign attest-blob \ + --yes \ + --predicate "${GENERATED_DIR}/predicate.json" \ + --type slsaprovenance1 \ + --bundle "${CALLER_DIST}/${BASENAME}.provenance.sigstore.json" \ + "$artifact" > /dev/null + echo "✅ Created ${BASENAME}.provenance.sigstore.json" + ((PROVENANCE_COUNT++)) || true + done + + echo "Generated provenance bundles: ${PROVENANCE_COUNT}" + if [ "$PROVENANCE_COUNT" -eq 0 ]; then + echo "::error::No provenance bundles were generated - this indicates a build problem" + exit 1 + fi + ls "${CALLER_DIST}"/*.provenance.sigstore.json + + - name: Sign SBOMs as attestation bundles + working-directory: _workflows + env: + CALLER_DIST: ../_caller/dist + shell: bash + run: | + set -euo pipefail + + SIGNED_COUNT=0 + + for sbom in "${CALLER_DIST}"/*.sbom.json; do + [ -f "$sbom" ] || continue + + SBOM_BASENAME=$(basename "$sbom") + ARCHIVE_NAME="${SBOM_BASENAME%.sbom.json}" + ARCHIVE="${CALLER_DIST}/${ARCHIVE_NAME}" + + if [ ! -f "$ARCHIVE" ]; then + echo "::error::Could not find archive for SBOM: $sbom (expected: $ARCHIVE)" + exit 1 + fi + + echo "Signing SBOM for: $(basename "$ARCHIVE")" + cosign attest-blob \ + --yes \ + --predicate "$sbom" \ + --type https://spdx.dev/Document \ + --bundle "${ARCHIVE}.sbom.sigstore.json" \ + "$ARCHIVE" > /dev/null + echo "✅ Created $(basename "$ARCHIVE").sbom.sigstore.json" + ((SIGNED_COUNT++)) || true + done + + echo "Generated SBOM bundles: ${SIGNED_COUNT}" + ls "${CALLER_DIST}"/*.sbom.sigstore.json 2>/dev/null || echo "ℹ️ No SBOM bundles generated (GoReleaser may not have generated SBOMs)" + + - name: Upload Linux release artifacts to S3 + working-directory: _workflows + env: + S3_BUCKET: ${{ env.S3_BUCKET }} + S3_DIRECTORY: ${{ steps.s3-directory.outputs.S3_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + ./scripts/upload-release-artifacts.sh \ + --bucket "$S3_BUCKET" \ + --directory "$S3_DIRECTORY" \ + --base-dir "../_caller/dist" + + # The GitHub Release does not exist yet: the macOS binaries job creates it, + # and the two jobs run concurrently. Hand the tarballs to + # publish-release-manifest, which runs after both, to attach them there. + - name: Stage Linux tarballs for the GitHub Release + uses: actions/upload-artifact@v4 + with: + name: linux-release-tarballs + path: _caller/dist/*.tar.gz + retention-days: 1 + if-no-files-found: error + + - name: Set up Go for workflows + uses: actions/setup-go@v6 + with: + go-version-file: "_workflows/go.mod" + cache: false + + - name: Generate Linux manifest + id: generate-linux-manifest + working-directory: _workflows + env: + CALLER_DIST: ../_caller/dist + run: | + # released_at is intentionally not passed: merge-manifests only consumes + # this manifest's assets map, and the authoritative release timestamp is + # read from the GitHub Release by the macOS binaries job. + MANIFEST_JSON=$(go run ./cmd/generate-manifest \ + -asset-dir "${CALLER_DIST}" \ + -repo-name "${{ github.event.repository.name }}" \ + -org-name "${{ github.event.repository.owner.login }}" \ + -tag "${{ inputs.tag }}" \ + -base-url "${{ env.CDN_BASE_URL }}/${{ steps.s3-directory.outputs.S3_DIRECTORY }}") + + # Debug output + echo "$MANIFEST_JSON" + + { + echo "linux_manifest<> "$GITHUB_OUTPUT" + + - name: Output checksums for merging + id: output-checksums + working-directory: _caller + run: | + # Find the checksums file generated by GoReleaser + CHECKSUMS_FILE=$(ls dist/*checksums*.txt 2>/dev/null | head -1) + if [ -z "$CHECKSUMS_FILE" ]; then + echo "::error::No checksums file found" + exit 1 + fi + + echo "Found checksums file: $CHECKSUMS_FILE" + + # Use randomized delimiter to prevent injection via filenames containing "EOF" + DELIM="CHECKSUMS_$(openssl rand -hex 8)" + { + echo "checksums<<${DELIM}" + cat "$CHECKSUMS_FILE" + echo "${DELIM}" + } >> "$GITHUB_OUTPUT" + goreleaser-windows: if: inputs.msi == true needs: [determine-workflows-ref, release-preflight] @@ -1216,11 +1497,11 @@ jobs: publish-release-manifest: # Release manifest publication: manifest + checksums + S3 upload. - # Require binaries to succeed; windows and docker may be skipped based on inputs. + # Require binaries and linux to succeed; windows and docker may be skipped based on inputs. # Each optional job must succeed if it ran — a failure means incomplete release artifacts. # see: https://docs.github.com/en/actions/using-jobs/using-conditions-to-control-job-execution - if: ${{ !cancelled() && needs.goreleaser-binaries.result == 'success' && (needs.goreleaser-windows.result == 'success' || needs.goreleaser-windows.result == 'skipped') && (needs.goreleaser-docker.result == 'success' || needs.goreleaser-docker.result == 'skipped') }} - needs: [determine-workflows-ref, goreleaser-binaries, goreleaser-windows, goreleaser-docker] + if: ${{ !cancelled() && needs.goreleaser-binaries.result == 'success' && needs.goreleaser-linux.result == 'success' && (needs.goreleaser-windows.result == 'success' || needs.goreleaser-windows.result == 'skipped') && (needs.goreleaser-docker.result == 'success' || needs.goreleaser-docker.result == 'skipped') }} + needs: [determine-workflows-ref, goreleaser-binaries, goreleaser-linux, goreleaser-windows, goreleaser-docker] outputs: merged_manifest: ${{ steps.export-manifest.outputs.merged_manifest }} manifest_url: ${{ steps.upload-manifest.outputs.manifest_url }} @@ -1256,10 +1537,11 @@ jobs: go-version-file: "_workflows/go.mod" cache: false - - name: Merge binaries, Windows, and images manifests + - name: Merge binaries, Linux, Windows, and images manifests working-directory: _workflows env: BINARIES_MANIFEST: ${{ needs.goreleaser-binaries.outputs.binaries_manifest }} + LINUX_MANIFEST: ${{ needs.goreleaser-linux.outputs.linux_manifest }} WINDOWS_MANIFEST: ${{ needs.goreleaser-windows.outputs.windows_manifest }} IMAGES_MANIFEST: ${{ needs.goreleaser-docker.outputs.images_manifest }} OUTPUT_DIR: _output @@ -1267,6 +1549,7 @@ jobs: mkdir -p "${OUTPUT_DIR}" go run ./cmd/merge-manifests \ -binaries-manifest "$BINARIES_MANIFEST" \ + -linux-manifest "$LINUX_MANIFEST" \ -windows-manifest "$WINDOWS_MANIFEST" \ -images-manifest "$IMAGES_MANIFEST" \ | tee "${OUTPUT_DIR}/manifest.json" @@ -1284,6 +1567,7 @@ jobs: working-directory: _workflows/_output env: BINARIES_CHECKSUMS: ${{ needs.goreleaser-binaries.outputs.binaries_checksums }} + LINUX_CHECKSUMS: ${{ needs.goreleaser-linux.outputs.linux_checksums }} WINDOWS_MANIFEST: ${{ needs.goreleaser-windows.outputs.windows_manifest }} REPO_NAME: ${{ github.event.repository.name }} VERSION: ${{ inputs.tag }} @@ -1299,6 +1583,15 @@ jobs: echo "Creating unified checksums file: $CHECKSUMS_FILE" echo "$BINARIES_CHECKSUMS" > "./${CHECKSUMS_FILE}" + # Append Linux tarball hashes. GoReleaser emits these in the Linux job's + # own checksums file, which is never uploaded on its own. + if [ -z "$LINUX_CHECKSUMS" ]; then + echo "::error::Linux checksums are empty" + exit 1 + fi + echo "Appending Linux hashes..." + echo "$LINUX_CHECKSUMS" >> "./${CHECKSUMS_FILE}" + # Append Windows asset hashes from manifest # Format: if [ -n "$WINDOWS_MANIFEST" ] && [ "$WINDOWS_MANIFEST" != "{}" ]; then @@ -1463,6 +1756,33 @@ jobs: --body "manifest.json.sigstore.json" \ --content-type "application/json" + # Restores the linux-* tarballs to the GitHub Release. Before the Linux + # build was split into its own job, GoReleaser attached them directly. + - name: Download Linux tarballs + uses: actions/download-artifact@v4 + with: + name: linux-release-tarballs + path: _linux_tarballs + + - name: Attach Linux tarballs to the GitHub Release + env: + GH_TOKEN: ${{ secrets.RELENG_GITHUB_TOKEN }} + TAG: ${{ inputs.tag }} + REPO: ${{ github.repository }} + shell: bash + run: | + set -euo pipefail + + shopt -s nullglob + tarballs=(_linux_tarballs/*.tar.gz) + if [ "${#tarballs[@]}" -eq 0 ]; then + echo "::error::No Linux tarballs were staged for the GitHub Release" + exit 1 + fi + + gh release upload "$TAG" "${tarballs[@]}" --repo "$REPO" --clobber + echo "✅ Attached ${#tarballs[@]} Linux tarballs to the $TAG release" + # ================================================================ # Registry API: record release after release manifest publication. # This is the sole release metadata recording path. @@ -1658,6 +1978,7 @@ jobs: [ determine-workflows-ref, goreleaser-binaries, + goreleaser-linux, goreleaser-windows, goreleaser-docker, publish-release-manifest, diff --git a/README.md b/README.md index d1767b0..6233318 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/cmd/merge-manifests/main.go b/cmd/merge-manifests/main.go index 95a4e64..3332341 100644 --- a/cmd/merge-manifests/main.go +++ b/cmd/merge-manifests/main.go @@ -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() @@ -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{ @@ -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": { ... } } diff --git a/docs/diagrams/release-workflow.dot b/docs/diagrams/release-workflow.dot index 1433f22..ba89e4c 100644 --- a/docs/diagrams/release-workflow.dot +++ b/docs/diagrams/release-workflow.dot @@ -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"]; @@ -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"]; diff --git a/docs/diagrams/release-workflow.png b/docs/diagrams/release-workflow.png index 3f2b98e..6527609 100644 Binary files a/docs/diagrams/release-workflow.png and b/docs/diagrams/release-workflow.png differ diff --git a/docs/release-workflow.md b/docs/release-workflow.md index 3c3f969..41e7be6 100644 --- a/docs/release-workflow.md +++ b/docs/release-workflow.md @@ -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) @@ -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) diff --git a/scripts/test-release-config-templates.py b/scripts/test-release-config-templates.py index 4d2f3d1..15217d9 100755 --- a/scripts/test-release-config-templates.py +++ b/scripts/test-release-config-templates.py @@ -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", @@ -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") diff --git a/scripts/test-release-workflow-tag-pin.sh b/scripts/test-release-workflow-tag-pin.sh index 8b7604e..5a9e878 100755 --- a/scripts/test-release-workflow-tag-pin.sh +++ b/scripts/test-release-workflow-tag-pin.sh @@ -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" diff --git a/templates/.goreleaser-binaries-template.yaml.tmpl b/templates/.goreleaser-binaries-template.yaml.tmpl index 922c093..816ca7b 100644 --- a/templates/.goreleaser-binaries-template.yaml.tmpl +++ b/templates/.goreleaser-binaries-template.yaml.tmpl @@ -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 @@ -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 @@ -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 @@ -74,7 +56,6 @@ signs: cmd: cosign artifacts: archive ids: - - linux-archive - darwin-archive certificate: "{{ .Env.artifact }}.cert" args: @@ -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}" diff --git a/templates/.goreleaser-linux-template.yaml.tmpl b/templates/.goreleaser-linux-template.yaml.tmpl new file mode 100644 index 0000000..4e149fe --- /dev/null +++ b/templates/.goreleaser-linux-template.yaml.tmpl @@ -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