Skip to content
Merged
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 ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ The `StreamWithPrefix` function reads output from subprocess pipes line-by-line
### `pkg/site`
* Assembles the static repository landing page.
* Fetches the current production index from the active Pages hosting to seed the update.
* Merges execution records from parallel runner cells.
* Merges execution records from parallel runner cells, preferring a branch-qualified cell over a legacy `<app-id>-<arch>` cell for the same app, architecture, and branch, so a stale legacy record cannot shadow a fresh one.
* Reconciles the index by validating digest existence via registry `HEAD` checks (pruning entries only on definitive 404s).
* Generates GPG public key material (`key.asc`), signing manifests (`signing.json`), `.flatpakrepo` configurations, and one-click `.flatpakref` installer files. Bypasses key export and GPG validation checks when `no_sign` is set to `true`, and enforces GPG key existence unless `allow_unsigned` is explicitly enabled.

Expand Down
48 changes: 45 additions & 3 deletions pkg/record/record.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,16 @@ func (r Record) CellDir(root string) (string, error) {
return "", err
}
if r.Branch == "" {
return filepath.Join(root, fmt.Sprintf("%s-%s", r.AppID, r.Arch)), nil
return filepath.Join(root, legacyCellName(r)), nil
}
return filepath.Join(root, fmt.Sprintf("%s-%s-%s", r.AppID, r.Branch, r.Arch)), nil
}

// legacyCellName is the pre-branch cell directory name (<app>-<arch>).
func legacyCellName(r Record) string {
return fmt.Sprintf("%s-%s", r.AppID, r.Arch)
}

// WriteRecord writes the record and labels into a cell directory under root.
func WriteRecord(root string, r Record, labels map[string]string) (string, error) {
cellDir, err := r.CellDir(root)
Expand Down Expand Up @@ -153,6 +158,11 @@ func IterRecords(root string) ([]RecordWithLabels, error) {
sort.Strings(cellDirs)

var results []RecordWithLabels
// seen maps a logical cell (app, arch, branch) to its index in results, so a
// legacy cell cannot shadow the branch-qualified cell that replaces it on
// disk. Both forms carry the same branch in JSON, so the directory shape is
// the only signal that tells them apart.
seen := make(map[cellKey]int, len(cellDirs))
for _, cellPath := range cellDirs {
recPath := filepath.Join(cellPath, "record.json")
lblPath := filepath.Join(cellPath, "labels.json")
Expand All @@ -179,16 +189,48 @@ func IterRecords(root string) ([]RecordWithLabels, error) {
return nil, fmt.Errorf("failed to parse labels JSON from %q: %w", lblPath, err)
}

results = append(results, RecordWithLabels{
rwl := RecordWithLabels{
Record: r,
Labels: labels,
Path: cellPath,
})
}

key := cellKey{AppID: r.AppID, Arch: r.Arch, Branch: r.Branch}
if idx, ok := seen[key]; ok {
// Prefer the branch-qualified cell over the legacy <app>-<arch>
// form. On equal preference the first cell wins, which keeps the
// result deterministic.
if isLegacyCell(results[idx].Path, results[idx].Record) && !isLegacyCell(rwl.Path, rwl.Record) {
results[idx] = rwl
}
continue
}
seen[key] = len(results)
results = append(results, rwl)
}

return results, nil
}

// cellKey identifies the logical cell a record belongs to, independent of the
// directory shape the writing version used.
type cellKey struct {
AppID string
Arch string
Branch string
}

// isLegacyCell reports whether a cell was written to the pre-branch path form
// (<app>-<arch>). Records written before the branch became part of the cell path
// still carry a branch in JSON, so the directory name is the only reliable
// signal.
func isLegacyCell(cellPath string, r Record) bool {
if r.Branch == "" {
return false
}
return filepath.Base(cellPath) == legacyCellName(r)
}

func fileExists(path string) bool {
info, err := os.Stat(path)
if err != nil {
Expand Down
136 changes: 136 additions & 0 deletions pkg/record/record_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package record

import (
"encoding/json"
"os"
"path/filepath"
"testing"
Expand Down Expand Up @@ -259,6 +260,141 @@ func TestIterRecordsRecursive(t *testing.T) {
}
}

// writeRawCell places a record and labels directly in cellDir, bypassing
// WriteRecord, so a test can reproduce the legacy <app>-<arch> layout a
// pre-branch version of this package produced.
func writeRawCell(t *testing.T, cellDir string, rec Record, labels map[string]string) {
t.Helper()
if err := os.MkdirAll(cellDir, 0755); err != nil {
t.Fatal(err)
}
recBytes, err := json.Marshal(rec)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(cellDir, "record.json"), recBytes, 0644); err != nil {
t.Fatal(err)
}
lblBytes, err := json.Marshal(labels)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(cellDir, "labels.json"), lblBytes, 0644); err != nil {
t.Fatal(err)
}
}

const (
freshDigest = "sha256:1111111111111111111111111111111111111111111111111111111111111111"
legacyDigest = "sha256:2222222222222222222222222222222222222222222222222222222222222222"
)

// TestIterRecordsPrefersBranchQualifiedCell covers both lexical orders between
// the legacy <app>-<arch> cell and the branch-qualified <app>-<branch>-<arch>
// cell: only the architecture decides whether the legacy cell is seen before or
// after its replacement, so both paths through the dedupe are exercised.
func TestIterRecordsPrefersBranchQualifiedCell(t *testing.T) {
const appID = "org.example.App"
const branch = "stable"

for _, arch := range []string{"x86_64", "aarch64"} {
t.Run(arch, func(t *testing.T) {
tempDir := t.TempDir()
ref := "app/" + appID + "/" + arch + "/" + branch

// Fresh cell written by the current code: <app>-<branch>-<arch>.
fresh := Record{
AppID: appID,
Arch: arch,
Branch: branch,
Name: "my-org/my-app",
Registry: "ghcr.io",
Digest: freshDigest,
Ref: ref,
Tag: "fresh",
}
freshLabels := map[string]string{
"org.flatpak.ref": ref,
"org.flatpak.commit": "fresh",
}
freshCell, err := WriteRecord(tempDir, fresh, freshLabels)
if err != nil {
t.Fatalf("failed to write fresh record: %v", err)
}
if filepath.Base(freshCell) != appID+"-"+branch+"-"+arch {
t.Fatalf("fresh cell = %q, want branch-qualified directory", freshCell)
}

// Legacy cell for the same app/arch/branch, written to the pre-branch
// path.
legacy := fresh
legacy.Digest = legacyDigest
legacy.Tag = "legacy"
writeRawCell(t, filepath.Join(tempDir, appID+"-"+arch), legacy, map[string]string{
"org.flatpak.ref": ref,
"org.flatpak.commit": "legacy",
})

records, err := IterRecords(tempDir)
if err != nil {
t.Fatalf("failed to iter records: %v", err)
}
if len(records) != 1 {
t.Fatalf("expected the legacy cell to be dropped, got %d records: %+v", len(records), records)
}
if records[0].Path != freshCell {
t.Errorf("surviving cell path = %q, want %q", records[0].Path, freshCell)
}
if records[0].Record.Digest != freshDigest {
t.Errorf("surviving digest = %q, want %q", records[0].Record.Digest, freshDigest)
}
if records[0].Labels["org.flatpak.commit"] != "fresh" {
t.Errorf("surviving labels = %v, want the branch-qualified cell's", records[0].Labels)
}
})
}
}

func TestIterRecordsKeepsLegacyCellWithoutCounterpart(t *testing.T) {
tempDir := t.TempDir()

const appID = "org.example.App"
const arch = "x86_64"
const branch = "stable"
ref := "app/" + appID + "/" + arch + "/" + branch

// A legacy cell with no branch-qualified sibling is still the only record for
// its app/arch/branch, so it must keep loading (backward compatibility).
legacyCell := filepath.Join(tempDir, appID+"-"+arch)
writeRawCell(t, legacyCell, Record{
AppID: appID,
Arch: arch,
Branch: branch,
Name: "my-org/my-app",
Registry: "ghcr.io",
Digest: legacyDigest,
Ref: ref,
Tag: "legacy",
}, map[string]string{
"org.flatpak.ref": ref,
"org.flatpak.commit": "legacy",
})

records, err := IterRecords(tempDir)
if err != nil {
t.Fatalf("failed to iter records: %v", err)
}
if len(records) != 1 {
t.Fatalf("expected 1 record, got %d", len(records))
}
if records[0].Path != legacyCell {
t.Errorf("surviving cell path = %q, want %q", records[0].Path, legacyCell)
}
if records[0].Record.Digest != legacyDigest {
t.Errorf("surviving digest = %q, want %q", records[0].Record.Digest, legacyDigest)
}
}

func TestWriteRecordSeparatesBranches(t *testing.T) {
tempDir := t.TempDir()

Expand Down
Loading
Loading