feat(release): support `direct_asset_path` and alias `filepath`

This commit is contained in:
Timo Furrer 2024-04-24 11:51:08 +00:00 committed by Tomas Vik
parent 2931bb3f49
commit a64a024937
8 changed files with 306 additions and 25 deletions

View File

@ -107,7 +107,7 @@ func NewCmdCreate(f *cmdutils.Factory) *cobra.Command {
"name": "Asset1",
"url":"https://<domain>/some/location/1",
"link_type": "other",
"filepath": "path/to/file"
"direct_asset_path": "path/to/file"
}
]'
`),
@ -160,7 +160,7 @@ func NewCmdCreate(f *cmdutils.Factory) *cobra.Command {
cmd.Flags().StringVarP(&opts.NotesFile, "notes-file", "F", "", "Read release notes `file`. Specify `-` as value to read from stdin")
cmd.Flags().StringVarP(&opts.ReleasedAt, "released-at", "D", "", "The `date` when the release is/was ready. Defaults to the current datetime. Expected in ISO 8601 format (2019-03-15T08:00:00Z)")
cmd.Flags().StringSliceVarP(&opts.Milestone, "milestone", "m", []string{}, "The title of each milestone the release is associated with")
cmd.Flags().StringVarP(&opts.AssetLinksAsJson, "assets-links", "a", "", "`JSON` string representation of assets links (e.g. `--assets-links='[{\"name\": \"Asset1\", \"url\":\"https://<domain>/some/location/1\", \"link_type\": \"other\", \"filepath\": \"path/to/file\"}]')`")
cmd.Flags().StringVarP(&opts.AssetLinksAsJson, "assets-links", "a", "", "`JSON` string representation of assets links (e.g. `--assets-links='[{\"name\": \"Asset1\", \"url\":\"https://<domain>/some/location/1\", \"link_type\": \"other\", \"direct_asset_path\": \"path/to/file\"}]')`")
return cmd
}

View File

@ -202,3 +202,99 @@ func TestReleaseCreateWithFiles(t *testing.T) {
})
}
}
func TestReleaseCreate_WithAssetsLinksJSON(t *testing.T) {
tests := []struct {
name string
cli string
expectedOutput string
}{
{
name: "with direct_asset_path",
cli: `0.0.1 --assets-links='[{"name": "any-name", "url": "https://example.com/any-asset-url", "direct_asset_path": "/any-path"}]'`,
expectedOutput: ` Validating tag 0.0.1
Creating or updating release repo=OWNER/REPO tag=0.0.1
Release created url=https://gitlab.com/OWNER/REPO/-/releases/0.0.1
Uploading release assets repo=OWNER/REPO tag=0.0.1
Added release asset name=any-name url=https://gitlab.example.com/OWNER/REPO/releases/0.0.1/downloads/any-path
Release succeeded after`,
},
{
name: "with filepath aliased to direct_asset_path",
cli: `0.0.1 --assets-links='[{"name": "any-name", "url": "https://example.com/any-asset-url", "filepath": "/any-path"}]'`,
expectedOutput: ` Validating tag 0.0.1
Creating or updating release repo=OWNER/REPO tag=0.0.1
Release created url=https://gitlab.com/OWNER/REPO/-/releases/0.0.1
Uploading release assets repo=OWNER/REPO tag=0.0.1
Added release asset name=any-name url=https://gitlab.example.com/OWNER/REPO/releases/0.0.1/downloads/any-path
! Aliased deprecated ` + "`filepath`" + ` field to ` + "`direct_asset_path`" + `. Replace ` + "`filepath`" + ` with ` + "`direct_asset_path`" + ` name=any-name
Release succeeded after`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fakeHTTP := &httpmock.Mocker{
MatchURL: httpmock.PathAndQuerystring,
}
defer fakeHTTP.Verify(t)
fakeHTTP.RegisterResponder(http.MethodGet, "/api/v4/projects/OWNER/REPO/releases/0%2E0%2E1",
httpmock.NewStringResponse(http.StatusNotFound, `{"message":"404 Not Found"}`))
fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/projects/OWNER/REPO/releases",
func(req *http.Request) (*http.Response, error) {
rb, _ := io.ReadAll(req.Body)
assert.NotContains(t, string(rb), `"direct_asset_path":`)
assert.NotContains(t, string(rb), `"filepath":`)
resp, _ := httpmock.NewStringResponse(http.StatusCreated, `
{
"name": "test_release",
"tag_name": "0.0.1",
"description": null,
"created_at": "2023-01-19T02:58:32.622Z",
"released_at": "2023-01-19T02:58:32.622Z",
"upcoming_release": false,
"tag_path": "/OWNER/REPO/-/tags/0.0.1",
"_links": {
"self": "https://gitlab.com/OWNER/REPO/-/releases/0.0.1"
}
}
`)(req)
return resp, nil
},
)
fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/projects/OWNER/REPO/releases/0%2E0%2E1/assets/links",
func(req *http.Request) (*http.Response, error) {
rb, _ := io.ReadAll(req.Body)
assert.Contains(t, string(rb), `"direct_asset_path":"/any-path"`)
assert.NotContains(t, string(rb), `"filepath":`)
resp, _ := httpmock.NewStringResponse(http.StatusCreated, `
{
"id":1,
"name":"any-name",
"url":"https://example.com/any-asset-url",
"direct_asset_url":"https://gitlab.example.com/OWNER/REPO/releases/0.0.1/downloads/any-path",
"link_type":"other"
}
`)(req)
return resp, nil
},
)
output, err := runCommand(fakeHTTP, false, tt.cli)
if assert.NoErrorf(t, err, "error running command `create %s`: %v", tt.cli, err) {
assert.Contains(t, output.Stderr(), tt.expectedOutput)
assert.Empty(t, output.String())
}
})
}
}

View File

@ -8,11 +8,28 @@ import (
"gitlab.com/gitlab-org/cli/pkg/iostreams"
)
// ConflictDirectAssetPathError is returned when both direct_asset_path and the deprecated filepath as specified for an asset link.
type ConflictDirectAssetPathError struct {
assetLinkName *string
}
func (e *ConflictDirectAssetPathError) Error() string {
var name string
if e.assetLinkName != nil {
name = *e.assetLinkName
} else {
name = "(without a name)"
}
return fmt.Sprintf("asset link %s contains both `direct_asset_path` and `filepath` (deprecated) fields. Remove the deprecated `filepath` field.", name)
}
type ReleaseAsset struct {
Name *string `json:"name,omitempty"`
URL *string `json:"url,omitempty"`
FilePath *string `json:"filepath,omitempty"`
LinkType *gitlab.LinkTypeValue `json:"link_type,omitempty"`
Name *string `json:"name,omitempty"`
URL *string `json:"url,omitempty"`
// Deprecated FilePath use DirectAssetPath instead.
FilePath *string `json:"filepath,omitempty"`
DirectAssetPath *string `json:"direct_asset_path,omitempty"`
LinkType *gitlab.LinkTypeValue `json:"link_type,omitempty"`
}
type ReleaseFile struct {
@ -23,17 +40,23 @@ type ReleaseFile struct {
Type *gitlab.LinkTypeValue
}
func CreateLink(c *gitlab.Client, projectID, tagName string, asset *ReleaseAsset) (*gitlab.ReleaseLink, error) {
func CreateLink(c *gitlab.Client, projectID, tagName string, asset *ReleaseAsset) (*gitlab.ReleaseLink, bool /* aliased deprecated filepath */, error) {
aliased, err := aliasFilePathToDirectAssetPath(asset)
if err != nil {
return nil, false, err
}
releaseLink, _, err := c.ReleaseLinks.CreateReleaseLink(projectID, tagName, &gitlab.CreateReleaseLinkOptions{
Name: asset.Name,
URL: asset.URL,
FilePath: asset.FilePath,
LinkType: asset.LinkType,
Name: asset.Name,
URL: asset.URL,
DirectAssetPath: asset.DirectAssetPath,
LinkType: asset.LinkType,
})
if err != nil {
return nil, err
return nil, false, err
}
return releaseLink, nil
return releaseLink, aliased, nil
}
type Context struct {
@ -75,11 +98,11 @@ func (c *Context) UploadFiles(projectID, tagName string) error {
linkURL := baseURL.String() + projectID + projectFile.URL
filename := "/" + file.Name
_, err = CreateLink(c.Client, projectID, tagName, &ReleaseAsset{
Name: &file.Label,
URL: &linkURL,
FilePath: &filename,
LinkType: file.Type,
_, _, err = CreateLink(c.Client, projectID, tagName, &ReleaseAsset{
Name: &file.Label,
URL: &linkURL,
DirectAssetPath: &filename,
LinkType: file.Type,
})
if err != nil {
return err
@ -96,15 +119,44 @@ func (c *Context) CreateReleaseAssetLinks(projectID string, tagName string) erro
}
color := c.IO.Color()
for _, asset := range c.AssetsLinks {
releaseLink, err := CreateLink(c.Client, projectID, tagName, asset)
releaseLink, aliased, err := CreateLink(c.Client, projectID, tagName, asset)
if err != nil {
return err
}
fmt.Fprintf(c.IO.StdErr, "%s Added release asset\t%s=%s %s=%s\n",
color.GreenCheck(), color.Blue("name"), *asset.Name,
color.Blue("url"), releaseLink.DirectAssetURL)
if aliased {
fmt.Fprintf(c.IO.StdErr, "\t%s Aliased deprecated `filepath` field to `direct_asset_path`. Replace `filepath` with `direct_asset_path`\t%s=%s\n",
color.WarnIcon(),
color.Blue("name"), *asset.Name)
}
}
c.AssetsLinks = nil
return nil
}
// aliasFilePathToDirectAssetPath ensures that the Asset Link uses the direct_asset_path and not the filepath.
// The filepath is deprecated and will be fully removed in GitLab 17.0.
// See https://docs.gitlab.com/ee/update/deprecations.html?removal_milestone=17.0#filepath-field-in-releases-and-release-links-apis
func aliasFilePathToDirectAssetPath(asset *ReleaseAsset) (bool /* aliased */, error) {
if asset.FilePath == nil || *asset.FilePath == "" {
// There is no deprecated filepath set, so we are good.
return false, nil
}
if asset.DirectAssetPath != nil && *asset.DirectAssetPath != "" {
// Both, filepath and direct_asset_path are set, so we have a conflict
return false, &ConflictDirectAssetPathError{asset.Name}
}
// We use the set filepath as direct_asset_path
// and clear the filepath to net send it via API.
asset.DirectAssetPath = asset.FilePath
asset.FilePath = nil
return true, nil
}

View File

@ -0,0 +1,59 @@
package upload
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/xanzy/go-gitlab"
)
func TestReleaseUtilsUpload_AliasFilePathToAssetDirectPath(t *testing.T) {
tests := []struct {
name string
givenReleaseAsset *ReleaseAsset
expectedReleaseAsset *ReleaseAsset
expectedAliased bool
}{
{
name: "no filepath, no direct_asset_path",
givenReleaseAsset: &ReleaseAsset{Name: gitlab.Ptr("any-name")},
expectedReleaseAsset: &ReleaseAsset{Name: gitlab.Ptr("any-name")},
expectedAliased: false,
},
{
name: "no filepath, but direct_asset_path",
givenReleaseAsset: &ReleaseAsset{Name: gitlab.Ptr("any-name"), DirectAssetPath: gitlab.Ptr("/any-path")},
expectedReleaseAsset: &ReleaseAsset{Name: gitlab.Ptr("any-name"), DirectAssetPath: gitlab.Ptr("/any-path")},
expectedAliased: false,
},
{
name: "filepath, but not direct_asset_path",
givenReleaseAsset: &ReleaseAsset{Name: gitlab.Ptr("any-name"), FilePath: gitlab.Ptr("/any-path")},
expectedReleaseAsset: &ReleaseAsset{Name: gitlab.Ptr("any-name"), DirectAssetPath: gitlab.Ptr("/any-path")},
expectedAliased: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
aliased, err := aliasFilePathToDirectAssetPath(tt.givenReleaseAsset)
assert.NoError(t, err)
assert.Equal(t, tt.expectedAliased, aliased)
assert.Equal(t, tt.expectedReleaseAsset, tt.givenReleaseAsset)
})
}
}
func TestReleaseUtilsUpload_AliasFilePathToAssetDirectPath_Conflict(t *testing.T) {
asset := &ReleaseAsset{
FilePath: gitlab.Ptr("/any-path"),
DirectAssetPath: gitlab.Ptr("/any-path"),
}
aliased, err := aliasFilePathToDirectAssetPath(asset)
target := &ConflictDirectAssetPathError{}
assert.ErrorAs(t, err, &target)
assert.False(t, aliased)
}

View File

@ -76,7 +76,7 @@ func NewCmdUpload(f *cmdutils.Factory) *cobra.Command {
"name": "Asset1",
"url":"https://<domain>/some/location/1",
"link_type": "other",
"filepath": "path/to/file"
"direct_asset_path": "path/to/file"
}
]'
`),
@ -107,7 +107,7 @@ func NewCmdUpload(f *cmdutils.Factory) *cobra.Command {
},
}
cmd.Flags().StringVarP(&opts.AssetLinksAsJson, "assets-links", "a", "", "`JSON` string representation of assets links (e.g. `--assets-links='[{\"name\": \"Asset1\", \"url\":\"https://<domain>/some/location/1\", \"link_type\": \"other\", \"filepath\": \"path/to/file\"}]')`")
cmd.Flags().StringVarP(&opts.AssetLinksAsJson, "assets-links", "a", "", "`JSON` string representation of assets links (e.g. `--assets-links='[{\"name\": \"Asset1\", \"url\":\"https://<domain>/some/location/1\", \"link_type\": \"other\", \"direct_asset_path\": \"path/to/file\"}]')`")
return cmd
}

View File

@ -117,3 +117,77 @@ func TestReleaseUpload(t *testing.T) {
})
}
}
func TestReleaseUpload_WithAssetsLinksJSON(t *testing.T) {
tests := []struct {
name string
cli string
expectedOutput string
}{
{
name: "with direct_asset_path",
cli: `0.0.1 --assets-links='[{"name": "any-name", "url": "https://example.com/any-asset-url", "direct_asset_path": "/any-path"}]'`,
expectedOutput: ` Validating tag repo=OWNER/REPO tag=0.0.1
Uploading release assets repo=OWNER/REPO tag=0.0.1
Added release asset name=any-name url=https://gitlab.example.com/OWNER/REPO/releases/0.0.1/downloads/any-path
Upload succeeded after`,
},
{
name: "with filepath aliased to direct_asset_path",
cli: `0.0.1 --assets-links='[{"name": "any-name", "url": "https://example.com/any-asset-url", "filepath": "/any-path"}]'`,
expectedOutput: ` Validating tag repo=OWNER/REPO tag=0.0.1
Uploading release assets repo=OWNER/REPO tag=0.0.1
Added release asset name=any-name url=https://gitlab.example.com/OWNER/REPO/releases/0.0.1/downloads/any-path
! Aliased deprecated ` + "`filepath`" + ` field to ` + "`direct_asset_path`" + `. Replace ` + "`filepath`" + ` with ` + "`direct_asset_path`" + ` name=any-name
Upload succeeded after`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fakeHTTP := &httpmock.Mocker{
MatchURL: httpmock.PathAndQuerystring,
}
defer fakeHTTP.Verify(t)
fakeHTTP.RegisterResponder(http.MethodGet, "/api/v4/projects/OWNER/REPO/releases/0%2E0%2E1", httpmock.NewStringResponse(http.StatusOK, `
{
"name": "test1",
"tag_name": "0.0.1",
"description": null,
"created_at": "2023-01-19T02:58:32.622Z",
"released_at": "2023-01-19T02:58:32.622Z",
"upcoming_release": false,
"tag_path": "/OWNER/REPO/-/tags/0.0.1"
}
`))
fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/projects/OWNER/REPO/releases/0%2E0%2E1/assets/links",
func(req *http.Request) (*http.Response, error) {
rb, _ := io.ReadAll(req.Body)
assert.Contains(t, string(rb), `"direct_asset_path":"/any-path"`)
assert.NotContains(t, string(rb), `"filepath":`)
resp, _ := httpmock.NewStringResponse(http.StatusCreated, `
{
"id":1,
"name":"any-name",
"url":"https://example.com/any-asset-url",
"direct_asset_url":"https://gitlab.example.com/OWNER/REPO/releases/0.0.1/downloads/any-path",
"link_type":"other"
}
`)(req)
return resp, nil
},
)
output, err := runCommand(fakeHTTP, false, tt.cli)
if assert.NoErrorf(t, err, "error running command `release upload %s`: %v", tt.cli, err) {
assert.Contains(t, output.Stderr(), tt.expectedOutput)
assert.Empty(t, output.String())
}
})
}
}

View File

@ -65,7 +65,7 @@ $ glab release create v1.0.1 --assets-links='
"name": "Asset1",
"url":"https://<domain>/some/location/1",
"link_type": "other",
"filepath": "path/to/file"
"direct_asset_path": "path/to/file"
}
]'
@ -74,7 +74,7 @@ $ glab release create v1.0.1 --assets-links='
## Options
```plaintext
-a, --assets-links JSON JSON string representation of assets links (e.g. `--assets-links='[{"name": "Asset1", "url":"https://<domain>/some/location/1", "link_type": "other", "filepath": "path/to/file"}]')`
-a, --assets-links JSON JSON string representation of assets links (e.g. `--assets-links='[{"name": "Asset1", "url":"https://<domain>/some/location/1", "link_type": "other", "direct_asset_path": "path/to/file"}]')`
-m, --milestone strings The title of each milestone the release is associated with
-n, --name string The release name or title
-N, --notes string The release notes/description. You can use Markdown

View File

@ -46,7 +46,7 @@ $ glab release upload v1.0.1 --assets-links='
"name": "Asset1",
"url":"https://<domain>/some/location/1",
"link_type": "other",
"filepath": "path/to/file"
"direct_asset_path": "path/to/file"
}
]'
@ -55,7 +55,7 @@ $ glab release upload v1.0.1 --assets-links='
## Options
```plaintext
-a, --assets-links JSON JSON string representation of assets links (e.g. `--assets-links='[{"name": "Asset1", "url":"https://<domain>/some/location/1", "link_type": "other", "filepath": "path/to/file"}]')`
-a, --assets-links JSON JSON string representation of assets links (e.g. `--assets-links='[{"name": "Asset1", "url":"https://<domain>/some/location/1", "link_type": "other", "direct_asset_path": "path/to/file"}]')`
```
## Options inherited from parent commands