From 5ee3c788c3eee2a53563fa1bc4880f838ef44012 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Wed, 4 Nov 2026 03:53:29 +1100 Subject: [PATCH 1/2] hardhat: make repoctl init support --ref-time This was missed when we unified the --ref-time handling and so new repositories always used the real wallclock rather than --ref-time. Fixes: ef7ac177d626 ("ctxext: unify --ref-time handling") Signed-off-by: Aleksa Sarai --- cmd/hardhat/repoctl.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/hardhat/repoctl.go b/cmd/hardhat/repoctl.go index 48a3c47..fe3bf46 100644 --- a/cmd/hardhat/repoctl.go +++ b/cmd/hardhat/repoctl.go @@ -99,6 +99,7 @@ var repoctlInitCommand = &cli.Command{ // Create an initial transaction to use. initTxn := tufrepo.InitTxn(signedRoot) + initTxn.RefTime, _ = ctxext.RefTime(ctx) // TODO: Make the initial transaction configurable... // TODO: Do we actually want to Sign this...? From cbedeccc596f1019611c8b610ccfcbc0f63e5bc9 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Wed, 4 Nov 2026 03:57:04 +1100 Subject: [PATCH 2/2] hardhat: make --ref-time default to $SOURCE_DATE_EPOCH tine and other build tools will want to generate TUF metadata in a reproducible way so we should obey $SOURCE_DATE_EPOCH if it is set to make things easier for them. Signed-off-by: Aleksa Sarai --- cmd/hardhat/main.go | 2 +- cmd/internal/cliext/reftime.go | 53 +++++++++++++- cmd/internal/cliext/reftime_test.go | 103 ++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 cmd/internal/cliext/reftime_test.go diff --git a/cmd/hardhat/main.go b/cmd/hardhat/main.go index 5fede9f..186a098 100644 --- a/cmd/hardhat/main.go +++ b/cmd/hardhat/main.go @@ -90,7 +90,7 @@ var app = cliext.WithRefTimeFlag(&cli.Command{ keystoreDriversCommand, repoctlCommand, }, -}) +}, cliext.WithSourceDateEpoch()) func Main(args []string) error { return app.Run(context.Background(), args) diff --git a/cmd/internal/cliext/reftime.go b/cmd/internal/cliext/reftime.go index 0547259..8e77d8a 100644 --- a/cmd/internal/cliext/reftime.go +++ b/cmd/internal/cliext/reftime.go @@ -5,6 +5,9 @@ package cliext import ( "context" + "fmt" + "os" + "strconv" "time" "github.com/urfave/cli/v3" @@ -12,12 +15,51 @@ import ( "go.amutable.dev/quarry/internal/ctxext" ) +// RefTimeOption configures the behaviour of [WithRefTimeFlag]. +type RefTimeOption func(*refTimeConfig) + +type refTimeConfig struct { + sourceDateEpoch bool +} + +// WithSourceDateEpoch makes [WithRefTimeFlag] fall back to the value of +// $SOURCE_DATE_EPOCH if --ref-time was not explicitly set. +// +// See . +func WithSourceDateEpoch() RefTimeOption { + return func(cfg *refTimeConfig) { cfg.sourceDateEpoch = true } +} + +// sourceDateEpoch parses $SOURCE_DATE_EPOCH or a zero [time.Time] if it was +// not present or empty. +func sourceDateEpoch() (time.Time, error) { + epochStr := os.Getenv("SOURCE_DATE_EPOCH") + if epochStr == "" { + return time.Time{}, nil + } + epoch, err := strconv.ParseUint(epochStr, 10, 63) + if err != nil { + return time.Time{}, fmt.Errorf("invalid $SOURCE_DATE_EPOCH value %q: %w", epochStr, err) + } + return time.Unix(int64(epoch), 0).UTC(), nil +} + // WithRefTimeFlag adds the --ref-time flag to [cli.Command]. -func WithRefTimeFlag(cmd *cli.Command) *cli.Command { +func WithRefTimeFlag(cmd *cli.Command, opts ...RefTimeOption) *cli.Command { + var cfg refTimeConfig + for _, opt := range opts { + opt(&cfg) + } + + usage := "used instead of the wallclock time for all operations" + if cfg.sourceDateEpoch { + usage += " (defaults to $SOURCE_DATE_EPOCH if set)" + } + cmd.Flags = append(cmd.Flags, &cli.TimestampFlag{ Name: "ref-time", - Usage: "used instead of the wallclock time for expiry checks", + Usage: usage, Config: cli.TimestampConfig{ Layouts: []string{ time.RFC3339, @@ -30,6 +72,13 @@ func WithRefTimeFlag(cmd *cli.Command) *cli.Command { cmd.Before = WrapBeforeFuncs(cmd.Before, func(ctx context.Context, cmd *cli.Command) (context.Context, error) { refTime := cmd.Timestamp("ref-time").UTC() + if cfg.sourceDateEpoch && !cmd.IsSet("ref-time") { + var err error + refTime, err = sourceDateEpoch() + if err != nil { + return ctx, err + } + } if !refTime.IsZero() { ctx = context.WithValue(ctx, ctxext.RefTimeCtxKey, refTime) } diff --git a/cmd/internal/cliext/reftime_test.go b/cmd/internal/cliext/reftime_test.go new file mode 100644 index 0000000..74c6174 --- /dev/null +++ b/cmd/internal/cliext/reftime_test.go @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Amutable GmbH + +package cliext + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/urfave/cli/v3" + + "go.amutable.dev/quarry/internal/ctxext" +) + +func runRefTime(t *testing.T, args []string, opts ...RefTimeOption) (time.Time, bool, error) { + t.Helper() + var ( + refTime time.Time + ok bool + ) + cmd := WithRefTimeFlag(&cli.Command{ + Name: "test", + Action: func(ctx context.Context, _ *cli.Command) error { + refTime, ok = ctxext.RefTime(ctx) + return nil + }, + }, opts...) + err := cmd.Run(context.Background(), append([]string{"test"}, args...)) + return refTime, ok, err +} + +func TestRefTimeFlag(t *testing.T) { + for _, test := range []struct { + name string + epoch string + args []string + opts []RefTimeOption + wantTime time.Time + wantOk bool + wantErr bool + }{ + {name: "Unset"}, + { + name: "Explicit", + args: []string{"--ref-time=2020-01-02T03:04:05Z"}, + wantTime: time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC), + wantOk: true, + }, + {name: "EpochIgnoredWithoutOpt", epoch: "1600000000"}, + { + name: "Epoch", + epoch: "1600000000", + opts: []RefTimeOption{WithSourceDateEpoch()}, + wantTime: time.Unix(1600000000, 0).UTC(), + wantOk: true, + }, + { + name: "EpochZero", + epoch: "0", + opts: []RefTimeOption{WithSourceDateEpoch()}, + wantTime: time.Unix(0, 0).UTC(), + wantOk: true, + }, + {name: "EpochEmpty", epoch: "", opts: []RefTimeOption{WithSourceDateEpoch()}}, + { + name: "ExplicitOverridesEpoch", + epoch: "1600000000", + args: []string{"--ref-time=2020-01-02T03:04:05Z"}, + opts: []RefTimeOption{WithSourceDateEpoch()}, + wantTime: time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC), + wantOk: true, + }, + { + name: "ExplicitOverridesBadEpoch", + epoch: "garbage", + args: []string{"--ref-time=2020-01-02"}, + opts: []RefTimeOption{WithSourceDateEpoch()}, + wantTime: time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC), + wantOk: true, + }, + {name: "EpochGarbage", epoch: "garbage", opts: []RefTimeOption{WithSourceDateEpoch()}, wantErr: true}, + {name: "EpochNegative", epoch: "-1", opts: []RefTimeOption{WithSourceDateEpoch()}, wantErr: true}, + {name: "EpochFractional", epoch: "1600000000.5", opts: []RefTimeOption{WithSourceDateEpoch()}, wantErr: true}, + } { + t.Run(test.name, func(t *testing.T) { + t.Setenv("SOURCE_DATE_EPOCH", test.epoch) + + refTime, ok, err := runRefTime(t, test.args, test.opts...) + if test.wantErr { + require.ErrorContains(t, err, "SOURCE_DATE_EPOCH") + return + } + require.NoError(t, err) + assert.Equal(t, test.wantOk, ok, "ref time set") + if test.wantOk { + assert.Equal(t, test.wantTime, refTime, "ref time") + } + }) + } +}