Sign or timestamp an unchanged tree's manifest in freshen (closes #190)
check / check (push) Waiting to run

freshen skipped the write whenever no file was added, changed or
removed, so --sign-key and --include-timestamps on an unchanged tree
did nothing. It now skips only when the manifest would come out the
same: nothing in the tree changed, the manifest is signed by the
--sign-key key (unsigned without one), and it has a createdAt
timestamp exactly when --include-timestamps asks for one. A file whose
recorded mode is not what --include-permissions asks for already
counts as changed.

Deviation: the mfer package gains SecretKeyFingerprint and a loaded
manifest's CreatedAt, which the comparison needs.

Model: opus-5-5
This commit is contained in:
2026-10-13 22:17:19 +00:00
parent 7542a68863
commit 0fd8b341b9
6 changed files with 175 additions and 17 deletions
+42 -17
View File
@@ -365,29 +365,39 @@ func writeFreshenedManifest(
}
// newFreshenBuilder constructs the manifest builder configured from CLI
// flags. A --sign-key given with an empty value is refused.
func (mfa *CLIApp) newFreshenBuilder(cmd *cli.Command) (*mfer.Builder, error) {
// flags, and returns the fingerprint of the --sign-key key, or "" when
// none is given. A --sign-key given with an empty value is refused.
func (mfa *CLIApp) newFreshenBuilder(
cmd *cli.Command,
) (*mfer.Builder, string, error) {
builder := mfer.NewBuilder()
if cmd.Bool("include-timestamps") {
builder.SetIncludeTimestamps(true)
}
var signKeyFingerprint string
// Set up signing options if sign-key is provided
if cmd.IsSet("sign-key") {
signKey := cmd.String("sign-key")
if signKey == "" {
return nil, errEmptySignKey
return nil, "", errEmptySignKey
}
signing, err := mfa.signingOptions(signKey)
if err != nil {
return nil, err
return nil, "", err
}
signKeyFingerprint, err = mfer.SecretKeyFingerprint(signing.SecretKey)
if err != nil {
return nil, "", fmt.Errorf("%s: %w", signKey, err)
}
builder.SetSigningOptions(signing)
}
return builder, nil
return builder, signKeyFingerprint, nil
}
// freshenScan runs the scan phase against the loaded manifest entries
@@ -499,12 +509,19 @@ func runFreshenHash(
return nil
}
// loadExistingEntries loads the manifest and indexes its file entries
// existingManifest is what freshen uses of the manifest it rewrites.
type existingManifest struct {
entriesByPath map[string]*mfer.MFFilePath
signer string // fingerprint of the key it is signed with, "" if unsigned
hasCreatedAt bool
}
// loadExistingManifest loads the manifest and indexes its file entries
// by path. It refuses a signed manifest when no --sign-key is given, as
// the rewritten manifest would be unsigned.
func (mfa *CLIApp) loadExistingEntries(
func (mfa *CLIApp) loadExistingManifest(
cmd *cli.Command, manifestPath string,
) (map[string]*mfer.MFFilePath, error) {
) (*existingManifest, error) {
log.Infof("loading manifest from %s", manifestPath)
// Load existing manifest
@@ -529,7 +546,11 @@ func (mfa *CLIApp) loadExistingEntries(
existingByPath[f.GetPath()] = f
}
return existingByPath, nil
return &existingManifest{
entriesByPath: existingByPath,
signer: string(manifest.Signer()),
hasCreatedAt: manifest.CreatedAt() != nil,
}, nil
}
func (mfa *CLIApp) freshenManifestOperation(
@@ -537,20 +558,18 @@ func (mfa *CLIApp) freshenManifestOperation(
) error {
log.Debug("freshenManifestOperation()")
showProgress := cmd.Bool("progress")
// Find manifest file
manifestPath, err := mfa.resolveFreshenManifestPath(cmd)
if err != nil {
return err
}
builder, err := mfa.newFreshenBuilder(cmd)
builder, signKeyFingerprint, err := mfa.newFreshenBuilder(cmd)
if err != nil {
return err
}
existingByPath, err := mfa.loadExistingEntries(cmd, manifestPath)
existing, err := mfa.loadExistingManifest(cmd, manifestPath)
if err != nil {
return err
}
@@ -562,7 +581,7 @@ func (mfa *CLIApp) freshenManifestOperation(
// Phase 1: Scan filesystem
scanner, removed, err := mfa.freshenScan(cmd, manifestPath, absBase,
existingByPath)
existing.entriesByPath)
if err != nil {
return err
}
@@ -579,7 +598,7 @@ func (mfa *CLIApp) freshenManifestOperation(
hasher := &freshenHasher{
fs: mfa.Fs,
absBase: absBase,
showProgress: showProgress,
showProgress: cmd.Bool("progress"),
totalHashBytes: totalHashBytes,
filesToHash: filesToHash,
startHash: time.Now(),
@@ -595,8 +614,14 @@ func (mfa *CLIApp) freshenManifestOperation(
log.Infof("freshen complete: %d unchanged, %d changed, %d added, %d removed",
scanner.unchanged, scanner.changed, scanner.added, removed)
// Skip writing if nothing changed
if scanner.changed == 0 && scanner.added == 0 && removed == 0 {
// Skip writing only when the manifest would come out the same: nothing
// in the tree changed, and the manifest already holds what --sign-key,
// --include-timestamps and --include-permissions ask for. The scan
// counts a file whose recorded mode is not what --include-permissions
// asks for as changed.
if scanner.changed == 0 && scanner.added == 0 && removed == 0 &&
existing.signer == signKeyFingerprint &&
existing.hasCreatedAt == cmd.Bool("include-timestamps") {
log.Infof("manifest unchanged, skipping write")
return nil