crucible build parity (#14)
test / test (push) Successful in 1m31s
build-binaries / build-binaries (push) Successful in 2m16s
build-image / publish (push) Successful in 57s

forces build parity with crucible = local builds and CI/CD builds use different tools.

Reviewed-on: #14
Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
This commit was merged in pull request #14.
This commit is contained in:
2026-06-21 13:17:58 +00:00
committed by archvillainette
parent faee2cde95
commit 3315f8b7eb
17 changed files with 2996 additions and 53 deletions
+41 -3
View File
@@ -366,6 +366,7 @@ type AutogenConsumerConfig struct {
Manifest AutogenManifestConfig `json:"manifest" yaml:"manifest"`
LocalOverrideRoot string `json:"local_override_root,omitempty" yaml:"local_override_root,omitempty"`
ManifestFile string `json:"manifest_file,omitempty" yaml:"manifest_file,omitempty"`
Source AutogenSourceConfig `json:"source,omitempty" yaml:"source,omitempty"`
}
type AccessoryVisualeffectsConfig struct {
@@ -416,6 +417,17 @@ type AutogenDeriveConfig struct {
GroupFrom string `json:"group_from,omitempty" yaml:"group_from,omitempty"`
}
type AutogenSourceConfig struct {
Kind string `json:"kind,omitempty" yaml:"kind,omitempty"`
CDNBase string `json:"cdn_base,omitempty" yaml:"cdn_base,omitempty"`
CDNBaseEnv string `json:"cdn_base_env,omitempty" yaml:"cdn_base_env,omitempty"`
ChannelsPath string `json:"channels_path,omitempty" yaml:"channels_path,omitempty"`
ManifestPath string `json:"manifest_path,omitempty" yaml:"manifest_path,omitempty"`
ReleaseMarkerPath string `json:"release_marker_path,omitempty" yaml:"release_marker_path,omitempty"`
ChannelEnv string `json:"channel_env,omitempty" yaml:"channel_env,omitempty"`
OfflineOverrideEnv string `json:"offline_override_env,omitempty" yaml:"offline_override_env,omitempty"`
}
type AutogenManifestConfig struct {
ReleaseTag string `json:"release_tag" yaml:"release_tag"`
AssetName string `json:"asset_name" yaml:"asset_name"`
@@ -641,6 +653,7 @@ func (p *Project) ValidateLayout() error {
failures = append(failures, validateTopDataRowGeneration(effective.TopData.RowGeneration)...)
failures = append(failures, validateTopDataRowExtensions(effective.TopData.RowExtensions)...)
failures = append(failures, validateTopDataClassFeatInjections(effective.TopData.ClassFeatInjections)...)
failures = append(failures, validateAutogenConsumerSources(effective.Autogen.Consumers)...)
failures = append(failures, validateRelativePath("topdata.compiled_2da_dir", effective.TopData.Compiled2DADir)...)
failures = append(failures, validateRelativePath("topdata.compiled_tlk", effective.TopData.CompiledTLK)...)
failures = append(failures, validateRelativePath("topdata.wiki.output_root", effective.TopData.Wiki.OutputRoot)...)
@@ -1669,10 +1682,12 @@ func validateAutogenConfig(cfg AutogenConfig) []error {
if strings.TrimSpace(consumer.Mode) == "accessory_visualeffects" && accessoryVisualeffectsConfigConfigured(consumer.AccessoryVisualeffects) {
failures = append(failures, validateAccessoryVisualeffectsConfig(fieldPrefix+".accessory_visualeffects", consumer.AccessoryVisualeffects)...)
}
// A manifest_file consumer reads a pre-resolved local manifest, so the
// released-source fields (root/include/derive/manifest) are never read
// A manifest_file consumer reads a pre-resolved local manifest, and a
// cdn_channel consumer resolves entirely from Source.* at runtime, so
// neither reads the released-source fields (root/include/derive/manifest)
// at build time — don't require them. See resolveAutogenConsumerManifest.
if strings.TrimSpace(consumer.ManifestFile) == "" {
if strings.TrimSpace(consumer.ManifestFile) == "" &&
strings.TrimSpace(consumer.Source.Kind) != "cdn_channel" {
if strings.TrimSpace(consumer.Root) == "" {
failures = append(failures, fmt.Errorf("%s.root is required", fieldPrefix))
}
@@ -1688,6 +1703,29 @@ func validateAutogenConfig(cfg AutogenConfig) []error {
return failures
}
func validateAutogenConsumerSources(consumers []AutogenConsumerConfig) []error {
var failures []error
for _, c := range consumers {
if strings.TrimSpace(c.Source.Kind) != "cdn_channel" {
continue
}
label := strings.TrimSpace(c.ID)
if label == "" {
label = "<unnamed>"
}
if strings.TrimSpace(c.Source.ChannelsPath) == "" {
failures = append(failures, fmt.Errorf("autogen consumer %s: source.channels_path is required for kind cdn_channel", label))
}
manifestPath := strings.TrimSpace(c.Source.ManifestPath)
if manifestPath == "" {
failures = append(failures, fmt.Errorf("autogen consumer %s: source.manifest_path is required for kind cdn_channel", label))
} else if !strings.Contains(manifestPath, "{tag}") {
failures = append(failures, fmt.Errorf("autogen consumer %s: source.manifest_path must contain {tag}", label))
}
}
return failures
}
func validateGeneratedConfig(cfg GeneratedConfig) []error {
var failures []error
seen := map[string]struct{}{}
+70
View File
@@ -1425,6 +1425,51 @@ func TestValidateLayoutRejectsInvalidAccessoryVisualeffectsNamingConfig(t *testi
}
}
func TestValidateLayoutAcceptsCDNChannelConsumerWithoutReleasedSourceFields(t *testing.T) {
// cdn_channel consumers resolve entirely from Source.* at runtime; they must
// not be required to supply root/include/derive/manifest fields.
root := t.TempDir()
mkdirAll(t, filepath.Join(root, "src"))
mkdirAll(t, filepath.Join(root, "build"))
proj := &Project{
Root: root,
Config: Config{
Module: ModuleConfig{Name: "Test", ResRef: "test"},
Paths: PathConfig{Source: "src", Build: "build"},
Autogen: AutogenConfig{
Consumers: []AutogenConsumerConfig{
{
ID: "accessory_visualeffects",
Producer: "accessory_visualeffects",
Dataset: "visualeffects",
Mode: "accessory_visualeffects",
Optional: true,
AccessoryVisualeffects: AccessoryVisualeffectsConfig{
Groups: map[string]AccessoryVisualeffectGroupConfig{
"head_accessories": {Prefix: "head_acc_"},
"chest_accessories": {Prefix: "chest_acc_"},
"head_decorations": {Prefix: "head_dec_"},
"head_features": {Prefix: "head_feat_"},
},
},
Source: AutogenSourceConfig{
Kind: "cdn_channel",
ChannelsPath: "releases/haks/channels.json",
ManifestPath: "releases/haks/{tag}/vfxs.yml",
},
// Intentionally omitted: Root, Include, Derive, Manifest
},
},
},
},
}
if err := proj.ValidateLayout(); err != nil {
t.Fatalf("cdn_channel consumer should pass ValidateLayout without released-source fields, got: %v", err)
}
}
func TestLoadRejectsLegacyAccessoryVisualeffectsNamingFields(t *testing.T) {
root := t.TempDir()
writeProjectFile(t, filepath.Join(root, ConfigFile), `
@@ -1796,6 +1841,31 @@ func TestCloneWithHAKNamesRejectsUnknownHAKs(t *testing.T) {
}
}
func TestValidateAutogenConsumerSourcesCDNChannel(t *testing.T) {
cases := []struct {
name string
source AutogenSourceConfig
wantFail bool
}{
{"valid", AutogenSourceConfig{Kind: "cdn_channel", ChannelsPath: "releases/haks/channels.json", ManifestPath: "releases/haks/{tag}/vfxs.yml"}, false},
{"missing channels_path", AutogenSourceConfig{Kind: "cdn_channel", ManifestPath: "releases/haks/{tag}/vfxs.yml"}, true},
{"missing manifest_path", AutogenSourceConfig{Kind: "cdn_channel", ChannelsPath: "releases/haks/channels.json"}, true},
{"manifest_path lacks {tag}", AutogenSourceConfig{Kind: "cdn_channel", ChannelsPath: "releases/haks/channels.json", ManifestPath: "releases/haks/vfxs.yml"}, true},
{"empty kind is ignored", AutogenSourceConfig{}, false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
failures := validateAutogenConsumerSources([]AutogenConsumerConfig{{ID: "x", Source: tc.source}})
if tc.wantFail && len(failures) == 0 {
t.Fatalf("expected validation failure, got none")
}
if !tc.wantFail && len(failures) != 0 {
t.Fatalf("expected no failure, got %v", failures)
}
})
}
}
func loadAndValidate(root string) error {
proj, err := Load(root)
if err != nil {