diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 74f9b27..8b84fb9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,13 +24,20 @@ jobs: fetch-depth: 0 # ARCHITECTURE: Core's tree-sitter projection requires CGO in every # downloadable agent, including cross-platform artifacts. + - name: Install Syft for archive SBOMs + id: syft + uses: anchore/sbom-action/download-syft@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 # v0.24.2 + with: + syft-version: v1.44.0 - name: Run CGO-capable GoReleaser run: >- docker run --rm -e GITHUB_TOKEN -v "$GITHUB_WORKSPACE:/workspace" + -v "$SYFT_PATH:/usr/local/bin/syft:ro" -w /workspace ghcr.io/goreleaser/goreleaser-cross:v1.27.1-1 release --clean env: GITHUB_TOKEN: ${{ secrets.GH_PAT }} + SYFT_PATH: ${{ steps.syft.outputs.cmd }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 7dbd422..6ece344 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,5 +1,12 @@ version: 2 +sboms: + - id: archives + artifacts: archive + documents: + - "${artifact}.sbom.json" + disable: false + project_name: service-rust builds: diff --git a/agent.codefly.yaml b/agent.codefly.yaml index aa638f1..1f841df 100644 --- a/agent.codefly.yaml +++ b/agent.codefly.yaml @@ -2,3 +2,6 @@ publisher: codefly.dev kind: codefly:service name: rust version: 0.0.37 +release: + owner: workflow + workflow: release.yml diff --git a/release_test.go b/release_test.go new file mode 100644 index 0000000..9d0acd8 --- /dev/null +++ b/release_test.go @@ -0,0 +1,62 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "gopkg.in/yaml.v3" +) + +func TestReleaseDeclaresOnePublisherAndArchiveSBOMs(t *testing.T) { + read := func(path string, target any) { + t.Helper() + payload, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if err := yaml.Unmarshal(payload, target); err != nil { + t.Fatal(err) + } + } + var manifest struct { + Release struct{ Owner, Workflow string } + } + read("agent.codefly.yaml", &manifest) + if manifest.Release.Owner != "workflow" || manifest.Release.Workflow != "release.yml" { + t.Fatal("the tag workflow must be the sole artifact publisher") + } + var config struct { + SBOMs []struct { + Artifacts string + Documents []string + Disable bool + } `yaml:"sboms"` + } + read(".goreleaser.yaml", &config) + if len(config.SBOMs) != 1 || config.SBOMs[0].Artifacts != "archive" || config.SBOMs[0].Disable || len(config.SBOMs[0].Documents) != 1 || config.SBOMs[0].Documents[0] != "${artifact}.sbom.json" { + t.Fatal("each published archive must carry its canonical SBOM") + } + var workflow struct { + Jobs map[string]struct { + Steps []struct { + ID, Uses, Run string + Env map[string]string + } + } + } + read(filepath.Join(".github", "workflows", manifest.Release.Workflow), &workflow) + installed, mounted := false, false + for _, step := range workflow.Jobs["release"].Steps { + if step.ID == "syft" && strings.HasPrefix(step.Uses, "anchore/sbom-action/download-syft@") { + installed = true + } + if strings.Contains(step.Run, `-v "$SYFT_PATH:/usr/local/bin/syft:ro"`) && step.Env["SYFT_PATH"] == "${{ steps.syft.outputs.cmd }}" { + mounted = true + } + } + if !installed || !mounted { + t.Fatal("the canonical cross build must have Syft installed and mounted") + } +}