Skip to content
Merged
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
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
- name: Configure private Go modules
Expand All @@ -23,5 +23,6 @@ jobs:
git config --global url."https://x-access-token:${RELEASES_TOKEN}@github.com/GoCodeAlone/".insteadOf "https://github.com/GoCodeAlone/"
go env -w GOPRIVATE=github.com/GoCodeAlone/*
go env -w GONOSUMDB=github.com/GoCodeAlone/*
- run: go run ./cmd/release-prep
- run: go test ./... -race -count=1
- run: go vet ./...
27 changes: 18 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,21 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 12
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
- name: Configure private Go modules
env:
RELEASES_TOKEN: ${{ secrets.RELEASES_TOKEN || github.token }}
run: |
git config --global url."https://x-access-token:${RELEASES_TOKEN}@github.com/GoCodeAlone/".insteadOf "https://github.com/GoCodeAlone/"
go env -w GOPRIVATE=github.com/GoCodeAlone/*
go env -w GONOSUMDB=github.com/GoCodeAlone/*
- name: Prepare release manifest
run: go run ./cmd/release-prep --tag "${{ github.ref_name }}" --write
Comment on lines +26 to +27
- name: Install wfctl v0.74.5
run: |
mkdir -p "${RUNNER_TEMP}/wfctl-bin"
Expand All @@ -22,10 +34,7 @@ jobs:
chmod +x "${RUNNER_TEMP}/wfctl-bin/wfctl"
- name: Validate plugin contract for publish (pre-build)
run: "${{ runner.temp }}/wfctl-bin/wfctl plugin validate-contract --for-publish --tag ${{ github.ref_name }} ."
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- uses: goreleaser/goreleaser-action@v7
- uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2
with:
distribution: goreleaser
version: '~> v2'
Expand Down Expand Up @@ -56,16 +65,16 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v6
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push product capture browser image
id: build
uses: docker/build-push-action@v6
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
file: docker/product-capture-browser/Dockerfile
Expand Down
22 changes: 22 additions & 0 deletions cmd/release-prep/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package main

import (
"flag"
"fmt"
"os"

"github.com/GoCodeAlone/workflow-plugin-product-capture/internal/releaseprep"
)

func main() {
var opts releaseprep.Options
flag.StringVar(&opts.ManifestPath, "manifest", "plugin.json", "plugin manifest path")
flag.StringVar(&opts.Tag, "tag", "", "release tag, defaults to v<plugin.json.version>")
flag.BoolVar(&opts.Write, "write", false, "rewrite plugin.json release metadata instead of checking it")
flag.Parse()

if err := releaseprep.Run(opts); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.26.0
require (
github.com/GoCodeAlone/workflow v0.74.5
github.com/GoCodeAlone/workflow-plugin-compute-core v0.4.0
golang.org/x/mod v0.36.0
golang.org/x/net v0.54.0
)

Expand Down
175 changes: 175 additions & 0 deletions internal/releaseprep/manifest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package releaseprep

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"strings"

"golang.org/x/mod/semver"
)

const releaseURLPrefix = "https://github.com/GoCodeAlone/workflow-plugin-product-capture/releases/download/"

type Manifest struct {
Name string `json:"name"`
Version string `json:"version"`
Description string `json:"description,omitempty"`
Author string `json:"author,omitempty"`
License string `json:"license,omitempty"`
Type string `json:"type,omitempty"`
Tier string `json:"tier,omitempty"`
Private bool `json:"private"`
MinEngineVersion string `json:"minEngineVersion,omitempty"`
Keywords []string `json:"keywords,omitempty"`
Homepage string `json:"homepage,omitempty"`
Repository string `json:"repository,omitempty"`
Capabilities json.RawMessage `json:"capabilities,omitempty"`
Contracts []json.RawMessage `json:"contracts,omitempty"`
Downloads []Download `json:"downloads,omitempty"`
raw map[string]json.RawMessage
}

type Download struct {
OS string `json:"os"`
Arch string `json:"arch"`
URL string `json:"url"`
}

type Options struct {
ManifestPath string
Tag string
Write bool
}

func Run(opts Options) error {
if opts.ManifestPath == "" {
opts.ManifestPath = "plugin.json"
}
manifest, err := Read(opts.ManifestPath)
if err != nil {
return err
}
tag := opts.Tag
if tag == "" {
tag = "v" + manifest.Version
}
updated, err := Prepare(manifest, tag)
if err != nil {
return err
}
if !opts.Write {
return Check(manifest, updated)
}
return Write(opts.ManifestPath, updated)
}

func Read(path string) (Manifest, error) {
data, err := os.ReadFile(path)
if err != nil {
return Manifest{}, err
}
var manifest Manifest
if err := json.NewDecoder(bytes.NewReader(data)).Decode(&manifest); err != nil {
return Manifest{}, fmt.Errorf("decode %s: %w", path, err)
}
if err := json.Unmarshal(data, &manifest.raw); err != nil {
return Manifest{}, fmt.Errorf("decode raw %s: %w", path, err)
}
return manifest, nil
}

func Prepare(manifest Manifest, tag string) (Manifest, error) {
version, err := versionFromTag(tag)
if err != nil {
return Manifest{}, err
}
if manifest.Name == "" {
return Manifest{}, errors.New("plugin.json.name is required")
}
if len(manifest.Downloads) == 0 {
return Manifest{}, errors.New("plugin.json.downloads is required")
}
manifest.Version = version
manifest.Downloads = append([]Download(nil), manifest.Downloads...)
for i := range manifest.Downloads {
dl := &manifest.Downloads[i]
if dl.OS == "" || dl.Arch == "" {
return Manifest{}, fmt.Errorf("downloads[%d] must declare os and arch", i)
}
dl.URL = fmt.Sprintf("%s%s/%s-%s-%s.tar.gz", releaseURLPrefix, tag, manifest.Name, dl.OS, dl.Arch)
}
return manifest, nil
}

func Check(current, expected Manifest) error {
var problems []string
if current.Version != expected.Version {
problems = append(problems, fmt.Sprintf("plugin.json.version=%q, want %q", current.Version, expected.Version))
}
if len(current.Downloads) != len(expected.Downloads) {
problems = append(problems, fmt.Sprintf("plugin.json.downloads has %d entries, want %d", len(current.Downloads), len(expected.Downloads)))
}
for i := range current.Downloads {
if i >= len(expected.Downloads) {
break
}
if current.Downloads[i] != expected.Downloads[i] {
problems = append(problems, fmt.Sprintf("plugin.json.downloads[%d]=%+v, want %+v", i, current.Downloads[i], expected.Downloads[i]))
}
}
if len(problems) > 0 {
return fmt.Errorf("release metadata is stale:\n- %s", strings.Join(problems, "\n- "))
}
return nil
}

func Write(path string, manifest Manifest) error {
fields, err := manifest.rawFields()
if err != nil {
return err
}
data, err := json.MarshalIndent(fields, "", " ")
if err != nil {
return err
}
data = append(data, '\n')
return os.WriteFile(path, data, 0o644)
}

func (manifest Manifest) rawFields() (map[string]json.RawMessage, error) {
fields := make(map[string]json.RawMessage, len(manifest.raw)+2)
if len(manifest.raw) > 0 {
for key, value := range manifest.raw {
fields[key] = value
}
} else {
data, err := json.Marshal(manifest)
if err != nil {
return nil, err
}
if err := json.Unmarshal(data, &fields); err != nil {
return nil, err
}
}
version, err := json.Marshal(manifest.Version)
if err != nil {
return nil, err
}
downloads, err := json.Marshal(manifest.Downloads)
if err != nil {
return nil, err
}
fields["version"] = version
fields["downloads"] = downloads
return fields, nil
}

func versionFromTag(tag string) (string, error) {
if !semver.IsValid(tag) || semver.Prerelease(tag) != "" || semver.Build(tag) != "" {
return "", fmt.Errorf("tag %q must be release semver vN.N.N", tag)
}
return strings.TrimPrefix(tag, "v"), nil
}
111 changes: 111 additions & 0 deletions internal/releaseprep/manifest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package releaseprep

import (
"os"
"path/filepath"
"strings"
"testing"
)

func TestPrepareUpdatesVersionAndDownloadURLs(t *testing.T) {
manifest := sampleManifest()

got, err := Prepare(manifest, "v0.2.0")
if err != nil {
t.Fatal(err)
}
if got.Version != "0.2.0" {
t.Fatalf("version = %q, want 0.2.0", got.Version)
}
wantURL := "https://github.com/GoCodeAlone/workflow-plugin-product-capture/releases/download/v0.2.0/workflow-plugin-product-capture-linux-amd64.tar.gz"
if got.Downloads[0].URL != wantURL {
t.Fatalf("download url = %q, want %q", got.Downloads[0].URL, wantURL)
}
}

func TestCheckRejectsStaleManifest(t *testing.T) {
current := sampleManifest()
expected, err := Prepare(current, "v0.2.0")
if err != nil {
t.Fatal(err)
}

err = Check(current, expected)
if err == nil {
t.Fatal("expected stale manifest error")
}
if !strings.Contains(err.Error(), "plugin.json.version") ||
!strings.Contains(err.Error(), "plugin.json.downloads[0]") {
t.Fatalf("error did not include stale fields: %v", err)
}
}

func TestRunWritesPreparedManifest(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "plugin.json")
if err := os.WriteFile(path, []byte(`{
"name": "workflow-plugin-product-capture",
"version": "0.1.11",
"futureField": {"kept": true},
"downloads": [
{
"os": "linux",
"arch": "amd64",
"url": "https://github.com/GoCodeAlone/workflow-plugin-product-capture/releases/download/v0.1.11/workflow-plugin-product-capture-linux-amd64.tar.gz"
}
]
}`), 0o644); err != nil {
t.Fatal(err)
}

if err := Run(Options{ManifestPath: path, Tag: "v0.2.0", Write: true}); err != nil {
t.Fatal(err)
}
got, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
for _, want := range []string{
`"version": "0.2.0"`,
`/releases/download/v0.2.0/`,
} {
if !strings.Contains(string(got), want) {
t.Fatalf("written manifest missing %q:\n%s", want, got)
}
}
if !strings.Contains(string(got), `"futureField":`) {
t.Fatalf("written manifest did not preserve unknown field:\n%s", got)
}
}

func TestPrepareRejectsNonReleaseTag(t *testing.T) {
for _, tag := range []string{"0.2.0", "v0.2.0-rc.1", "v0.2.0+build"} {
if _, err := Prepare(sampleManifest(), tag); err == nil {
t.Fatalf("Prepare(%q) succeeded, want error", tag)
}
}
}

func sampleManifest() Manifest {
return Manifest{
Name: "workflow-plugin-product-capture",
Version: "0.1.11",
Description: "Product URL capture provider for workflow-compute",
Author: "GoCodeAlone",
License: "MIT",
Type: "external",
Tier: "community",
MinEngineVersion: "0.57.4",
Keywords: []string{"product-capture"},
Homepage: "https://github.com/GoCodeAlone/workflow-plugin-product-capture",
Repository: "https://github.com/GoCodeAlone/workflow-plugin-product-capture",
Capabilities: []byte(`{"stepTypes":["step.product_capture"]}`),
Downloads: []Download{
{
OS: "linux",
Arch: "amd64",
URL: "https://github.com/GoCodeAlone/workflow-plugin-product-capture/releases/download/v0.1.11/workflow-plugin-product-capture-linux-amd64.tar.gz",
},
},
}
}
Loading