Add optional s3-acl input for canned ACLs on uploads

Lets the workflow set, e.g., public-read on the uploaded object so the
HTTPS URL is actually downloadable without further configuration. Empty
default means no ACL is sent — required for modern AWS buckets with
Object Ownership = "Bucket owner enforced" that reject any ACL.

Validates the value against the AWS canned-ACL list at config time so
typos fail before the upload runs. Wires the input through action.yml,
config, and the orchestrator; adds a unit test that the ACL is forwarded
to PutObjectInput when set and omitted when empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leon Mika 2026-05-02 13:59:49 +10:00
parent 78f63e640f
commit 03da0c3e85
6 changed files with 53 additions and 3 deletions

View file

@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go-v2/aws"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
)
// PutObjectAPI is the subset of *s3.Client used here. Tests inject a fake.
@ -21,6 +22,10 @@ type Opts struct {
Bucket string
Key string
FilePath string
// ACL, if set, is applied to the uploaded object as a canned ACL
// (e.g. "public-read"). Empty means no ACL is sent — required for
// buckets with Object Ownership = "Bucket owner enforced".
ACL string
}
// RenderKey substitutes {version} and {filename} placeholders.
@ -37,11 +42,15 @@ func Upload(ctx context.Context, c PutObjectAPI, o Opts) (string, error) {
}
defer f.Close()
if _, err := c.PutObject(ctx, &s3.PutObjectInput{
in := &s3.PutObjectInput{
Bucket: aws.String(o.Bucket),
Key: aws.String(o.Key),
Body: f,
}); err != nil {
}
if o.ACL != "" {
in.ACL = types.ObjectCannedACL(o.ACL)
}
if _, err := c.PutObject(ctx, in); err != nil {
return "", fmt.Errorf("s3 put: %w", err)
}
return fmt.Sprintf("s3://%s/%s", o.Bucket, o.Key), nil

View file

@ -104,6 +104,34 @@ func TestUpload_CallsPutObjectWithCorrectInputs(t *testing.T) {
}
}
func TestUpload_ForwardsACLWhenSet(t *testing.T) {
tmp := writeTempFile(t, "hello")
c := &fakePut{}
if _, err := upload.Upload(context.Background(), c, upload.Opts{
Bucket: "b", Key: "k", FilePath: tmp, ACL: "public-read",
}); err != nil {
t.Fatalf("unexpected: %v", err)
}
if string(c.calls[0].ACL) != "public-read" {
t.Fatalf("ACL got %q want public-read", c.calls[0].ACL)
}
}
func TestUpload_OmitsACLWhenEmpty(t *testing.T) {
tmp := writeTempFile(t, "hello")
c := &fakePut{}
if _, err := upload.Upload(context.Background(), c, upload.Opts{
Bucket: "b", Key: "k", FilePath: tmp,
}); err != nil {
t.Fatalf("unexpected: %v", err)
}
if c.calls[0].ACL != "" {
t.Fatalf("ACL got %q want empty (no ACL sent)", c.calls[0].ACL)
}
}
func TestUpload_FailsWhenFileMissing(t *testing.T) {
_, err := upload.Upload(context.Background(), &fakePut{}, upload.Opts{
Bucket: "b",