From 514ca4ae4b968ae1b2c5a8ebbdfca11f27784718 Mon Sep 17 00:00:00 2001 From: Eddie Knight Date: Tue, 18 Aug 2026 13:08:38 -0500 Subject: [PATCH 1/2] feat: publish this plugin to grc.store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Declare the publication metadata `pvtr publish` reads out of the built binary: Publisher ossf (coordinate ossf/github-repo), the Apache-2.0 license grc.store requires on every publication, and the namespace that owns each embedded catalog. The vendored OSPS Baseline YAML carries no metadata.author.id and pvtr will not guess an owner, so the mapping is declared here. None of it is reachable at run time, so the orchestrator setup moves into newOrchestrator and a test asserts the resulting publish manifest — otherwise a wrong coordinate or a newly embedded, unmapped catalog would first surface as a failed release. Add the publish workflow itself. It stays workflow_dispatch-only: the first successful publish TOFU-pins the signer identity to this file's path, so it must be deliberate and every later publish must run from here. PVTR_VERSION is a placeholder until the pvtr release carrying `pvtr publish` is cut. Signed-off-by: Eddie Knight --- .github/workflows/publish-plugin.yaml | 66 +++++++++++++++++++++++++ main.go | 70 +++++++++++++++++---------- main_test.go | 33 +++++++++++++ 3 files changed, 144 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/publish-plugin.yaml create mode 100644 main_test.go diff --git a/.github/workflows/publish-plugin.yaml b/.github/workflows/publish-plugin.yaml new file mode 100644 index 00000000..e98cd4b1 --- /dev/null +++ b/.github/workflows/publish-plugin.yaml @@ -0,0 +1,66 @@ +--- +name: "Publish plugin to grc.store" + +# Manual-only on purpose. The first successful publish TOFU-pins the signer +# identity of ossf/github-repo to THIS workflow file's path, so it has to be a +# deliberate act and every later publish must come from this same file. Add a +# `release: {types: [published]}` trigger once the manual run is proven. +on: + workflow_dispatch: + inputs: + ref: + description: "Tag or ref to build and publish (e.g. v0.28.0)" + required: true + hub-url: + description: "grc.store hub to publish to" + required: true + default: "https://hub.preview.grc.store" + +permissions: + contents: read + +jobs: + publish: + name: publish + runs-on: ubuntu-latest + permissions: + contents: read + # Mints two distinct OIDC tokens: the hub push token (audience = + # ci_audience) and the Sigstore signing identity (audience = sigstore, + # requested by pvtr itself). The hub must list this repo as a + # ci_publishers trusted publisher for the ossf namespace. + id-token: write + env: + # Must be a pvtr release that contains `pvtr publish` — see REV-345. + PVTR_VERSION: REPLACE_WITH_PVTR_RELEASE_TAG + PVTR_HUB_URL: ${{ inputs.hub-url }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.ref }} + persist-credentials: false + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + - name: install pvtr + run: go install github.com/privateerproj/privateer@${PVTR_VERSION} + - uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 + with: + version: "~> v2" + args: build --clean + - name: publish + # The hub token is minted and consumed inside this one step so it is + # never written to the job environment. pvtr reads the plugin's + # coordinate, license and catalog linkage from the built binary itself. + run: | + audience=$(curl -fsSL "${PVTR_HUB_URL}/.well-known/grc-store-configuration" | jq -r '.ci_audience') + if [ -z "$audience" ] || [ "$audience" = "null" ]; then + echo "hub ${PVTR_HUB_URL} advertises no ci_audience" >&2 + exit 1 + fi + PVTR_TOKEN=$(curl -fsSL \ + -H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ + "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=${audience}" | jq -r '.value') + echo "::add-mask::${PVTR_TOKEN}" + export PVTR_TOKEN + privateer publish --dist dist diff --git a/main.go b/main.go index d6a46718..b5c972e5 100644 --- a/main.go +++ b/main.go @@ -33,6 +33,14 @@ var ( "repo", "token", } + // catalogNamespaces declares the grc.store namespace that owns each embedded + // catalog. The OSPS Baseline is OSSF-owned, but the vendored YAML carries no + // metadata.author.id and `pvtr publish` refuses to guess an owner. + catalogNamespaces = map[string]string{ + "osps-baseline": "ossf", + "osps-baseline-2025-10": "ossf", + "osps-baseline-2026-02": "ossf", + } //go:embed data/catalogs files embed.FS dataDir = filepath.Join("data", "catalogs") @@ -43,10 +51,38 @@ func main() { Version = fmt.Sprintf("%s-%s", Version, VersionPostfix) } - orchestrator := pluginkit.EvaluationOrchestrator{ - PluginName: PluginName, - PluginVersion: Version, - PluginUri: "https://github.com/ossf/pvtr-github-repo-scanner", + orchestrator, err := newOrchestrator(Version) + if err != nil { + fmt.Printf("%v\n", err) + os.Exit(shared.InternalError) + } + + runCmd := command.NewPluginCommands( + PluginName, + Version, + VersionPostfix, + GitCommitHash, + orchestrator, + ) + + err = runCmd.Execute() + if err != nil { + os.Exit(shared.InternalError) + } +} + +// newOrchestrator builds the fully populated orchestrator for the given plugin +// version. Publisher, License and catalogNamespaces are inert at run time and +// exist so `pvtr publish` can derive the plugin's grc.store coordinate +// (ossf/github-repo) and its catalog linkage from the binary itself. +func newOrchestrator(version string) (*pluginkit.EvaluationOrchestrator, error) { + orchestrator := &pluginkit.EvaluationOrchestrator{ + PluginName: PluginName, + PluginVersion: version, + PluginUri: "https://github.com/ossf/pvtr-github-repo-scanner", + Publisher: "ossf", + License: "Apache-2.0", + CatalogNamespaces: catalogNamespaces, } orchestrator.AddLoader(data.Loader) orchestrator.AddTargetBuilder(func(c *config.Config) gemara.Resource { @@ -59,30 +95,14 @@ func main() { } }) - err := orchestrator.AddReferenceCatalogs(dataDir, files) - if err != nil { - fmt.Printf("Error loading catalog: %v\n", err) - os.Exit(shared.InternalError) + if err := orchestrator.AddReferenceCatalogs(dataDir, files); err != nil { + return nil, fmt.Errorf("error loading catalog: %w", err) } orchestrator.AddRequiredVars(RequiredVars) - err = pluginkit.AddEvaluationSuiteTypedForAllCatalogs(&orchestrator, nil, evaluation_plans.AllSteps()) - if err != nil { - fmt.Printf("Error adding evaluation suites: %v\n", err) - os.Exit(shared.InternalError) - } - - runCmd := command.NewPluginCommands( - PluginName, - Version, - VersionPostfix, - GitCommitHash, - &orchestrator, - ) - - err = runCmd.Execute() - if err != nil { - os.Exit(shared.InternalError) + if err := pluginkit.AddEvaluationSuiteTypedForAllCatalogs(orchestrator, nil, evaluation_plans.AllSteps()); err != nil { + return nil, fmt.Errorf("error adding evaluation suites: %w", err) } + return orchestrator, nil } diff --git a/main_test.go b/main_test.go new file mode 100644 index 00000000..254b54e5 --- /dev/null +++ b/main_test.go @@ -0,0 +1,33 @@ +package main + +import "testing" + +// The publish metadata (Publisher, License, catalogNamespaces) is inert at run +// time, so nothing else in this repo notices when it is wrong or when a newly +// embedded catalog is missing a namespace — the release publish is the first +// thing that would fail. Assert the manifest `pvtr publish` reads instead. +func TestPublishManifest(t *testing.T) { + orchestrator, err := newOrchestrator("1.2.3") + if err != nil { + t.Fatalf("newOrchestrator: %v", err) + } + + manifest, err := orchestrator.PublishManifest() + if err != nil { + t.Fatalf("PublishManifest: %v", err) + } + if manifest.Coordinate != "ossf/github-repo" { + t.Errorf("coordinate = %q, want ossf/github-repo", manifest.Coordinate) + } + if manifest.License != "Apache-2.0" { + t.Errorf("license = %q, want Apache-2.0", manifest.License) + } + if len(manifest.Evaluates) == 0 { + t.Fatal("expected at least one evaluated catalog") + } + for _, e := range manifest.Evaluates { + if len(e.Catalog) < len("ossf/") || e.Catalog[:len("ossf/")] != "ossf/" { + t.Errorf("catalog %q is not namespaced under ossf", e.Catalog) + } + } +} From 449652d7023c100f6b6b950b0489f78565e6d818 Mon Sep 17 00:00:00 2001 From: Eddie Knight Date: Tue, 18 Aug 2026 18:10:18 -0500 Subject: [PATCH 2/2] refactor: apply review feedback on publish metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trim the added comments to the one-line style used elsewhere in main.go, and drop the sentences that restate either the SDK's own field docs or the code on the next line. Tighten the publish step: it ran under GitHub's default `bash -e`, with no pipefail, so a failed token mint left PVTR_TOKEN empty (or the literal "null", which pvtr's TrimSpace accepts) and the step still reached `privateer publish`. Set -euo pipefail and guard the token the same way the audience above it was already guarded. `--dist` is the flag default, so drop it. Assert the exact catalog count in the publish manifest test — `== 0` only caught losing every catalog, not one dropping out of the embed glob — and use strings.HasPrefix over the hand-rolled slice compare. While here, fix the NewPluginCommands arguments: they were shifted a slot, so VersionPostfix landed in buildGitCommitHash and GitCommitHash in buildTime, and BuiltAt — set by both goreleaser and the Makefile — was passed nowhere. Version already carries the postfix. Signed-off-by: Eddie Knight --- .github/workflows/publish-plugin.yaml | 14 +++++++++----- main.go | 18 +++++++----------- main_test.go | 13 ++++++++----- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/.github/workflows/publish-plugin.yaml b/.github/workflows/publish-plugin.yaml index e98cd4b1..e3045e3c 100644 --- a/.github/workflows/publish-plugin.yaml +++ b/.github/workflows/publish-plugin.yaml @@ -31,7 +31,7 @@ jobs: # ci_publishers trusted publisher for the ossf namespace. id-token: write env: - # Must be a pvtr release that contains `pvtr publish` — see REV-345. + # TODO: must be a pvtr release that contains `pvtr publish` — see REV-345. PVTR_VERSION: REPLACE_WITH_PVTR_RELEASE_TAG PVTR_HUB_URL: ${{ inputs.hub-url }} steps: @@ -49,10 +49,10 @@ jobs: version: "~> v2" args: build --clean - name: publish - # The hub token is minted and consumed inside this one step so it is - # never written to the job environment. pvtr reads the plugin's - # coordinate, license and catalog linkage from the built binary itself. + # pvtr reads the plugin's coordinate, license and catalog linkage from + # the built binary itself. run: | + set -euo pipefail audience=$(curl -fsSL "${PVTR_HUB_URL}/.well-known/grc-store-configuration" | jq -r '.ci_audience') if [ -z "$audience" ] || [ "$audience" = "null" ]; then echo "hub ${PVTR_HUB_URL} advertises no ci_audience" >&2 @@ -61,6 +61,10 @@ jobs: PVTR_TOKEN=$(curl -fsSL \ -H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=${audience}" | jq -r '.value') + if [ -z "$PVTR_TOKEN" ] || [ "$PVTR_TOKEN" = "null" ]; then + echo "failed to mint a hub OIDC token for audience ${audience}" >&2 + exit 1 + fi echo "::add-mask::${PVTR_TOKEN}" export PVTR_TOKEN - privateer publish --dist dist + privateer publish diff --git a/main.go b/main.go index b5c972e5..a00d14ad 100644 --- a/main.go +++ b/main.go @@ -33,9 +33,7 @@ var ( "repo", "token", } - // catalogNamespaces declares the grc.store namespace that owns each embedded - // catalog. The OSPS Baseline is OSSF-owned, but the vendored YAML carries no - // metadata.author.id and `pvtr publish` refuses to guess an owner. + // The vendored Baseline YAML carries no metadata.author.id, so declare the owner. catalogNamespaces = map[string]string{ "osps-baseline": "ossf", "osps-baseline-2025-10": "ossf", @@ -51,7 +49,7 @@ func main() { Version = fmt.Sprintf("%s-%s", Version, VersionPostfix) } - orchestrator, err := newOrchestrator(Version) + orchestrator, err := newOrchestrator() if err != nil { fmt.Printf("%v\n", err) os.Exit(shared.InternalError) @@ -60,8 +58,8 @@ func main() { runCmd := command.NewPluginCommands( PluginName, Version, - VersionPostfix, GitCommitHash, + BuiltAt, orchestrator, ) @@ -71,14 +69,12 @@ func main() { } } -// newOrchestrator builds the fully populated orchestrator for the given plugin -// version. Publisher, License and catalogNamespaces are inert at run time and -// exist so `pvtr publish` can derive the plugin's grc.store coordinate -// (ossf/github-repo) and its catalog linkage from the binary itself. -func newOrchestrator(version string) (*pluginkit.EvaluationOrchestrator, error) { +// newOrchestrator builds the orchestrator. Publisher, License and +// catalogNamespaces are unused at run time; `pvtr publish` reads them. +func newOrchestrator() (*pluginkit.EvaluationOrchestrator, error) { orchestrator := &pluginkit.EvaluationOrchestrator{ PluginName: PluginName, - PluginVersion: version, + PluginVersion: Version, PluginUri: "https://github.com/ossf/pvtr-github-repo-scanner", Publisher: "ossf", License: "Apache-2.0", diff --git a/main_test.go b/main_test.go index 254b54e5..f3440bb3 100644 --- a/main_test.go +++ b/main_test.go @@ -1,13 +1,16 @@ package main -import "testing" +import ( + "strings" + "testing" +) // The publish metadata (Publisher, License, catalogNamespaces) is inert at run // time, so nothing else in this repo notices when it is wrong or when a newly // embedded catalog is missing a namespace — the release publish is the first // thing that would fail. Assert the manifest `pvtr publish` reads instead. func TestPublishManifest(t *testing.T) { - orchestrator, err := newOrchestrator("1.2.3") + orchestrator, err := newOrchestrator() if err != nil { t.Fatalf("newOrchestrator: %v", err) } @@ -22,11 +25,11 @@ func TestPublishManifest(t *testing.T) { if manifest.License != "Apache-2.0" { t.Errorf("license = %q, want Apache-2.0", manifest.License) } - if len(manifest.Evaluates) == 0 { - t.Fatal("expected at least one evaluated catalog") + if len(manifest.Evaluates) != len(catalogNamespaces) { + t.Fatalf("evaluated %d catalogs, want %d", len(manifest.Evaluates), len(catalogNamespaces)) } for _, e := range manifest.Evaluates { - if len(e.Catalog) < len("ossf/") || e.Catalog[:len("ossf/")] != "ossf/" { + if !strings.HasPrefix(e.Catalog, "ossf/") { t.Errorf("catalog %q is not namespaced under ossf", e.Catalog) } }