Skip to content
Open
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
2 changes: 1 addition & 1 deletion cmd/hardhat/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions cmd/hardhat/repoctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...?
Expand Down
53 changes: 51 additions & 2 deletions cmd/internal/cliext/reftime.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,61 @@ package cliext

import (
"context"
"fmt"
"os"
"strconv"
"time"

"github.com/urfave/cli/v3"

"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 <https://reproducible-builds.org/specs/source-date-epoch/>.
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,
Expand All @@ -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)
}
Expand Down
103 changes: 103 additions & 0 deletions cmd/internal/cliext/reftime_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
})
}
}
Loading