Skip to content

Fix full-only Surge promotion metadata#102

Merged
peters merged 1 commit into
mainfrom
fix/full-only-promotion
May 6, 2026
Merged

Fix full-only Surge promotion metadata#102
peters merged 1 commit into
mainfrom
fix/full-only-promotion

Conversation

@peters
Copy link
Copy Markdown
Contributor

@peters peters commented May 6, 2026

Summary

  • write a validated metadata sidecar next to full packages produced by surge pack
  • have surge push read that sidecar and persist the original full archive compression level/workers in releases.yml
  • keep older package directories compatible by falling back to UNRECORDED when no sidecar exists

Behavior impact

Full-only checkpoint releases produced through surge pack and later published through surge push now keep enough encoding metadata for surge promote to build channel-aware deltas. This prevents the prior failure where a release with no primary delta and UNRECORDED full encoding could not be promoted from test to production.

If a metadata sidecar exists but does not match the expected app, version, RID, archive filename, size, or SHA-256, surge push now rejects it instead of recording stale encoding settings.

Validation

  • ./scripts/sync-surge-core-vendor.sh --check
  • ./scripts/check-version-sync.sh
  • cargo fmt --all -- --check
  • cargo test -p surge-cli execute_records_full_encoding_from_package_metadata
  • cargo test -p surge-cli execute_pack_uses_default_dot_surge_artifacts_layout
  • RUSTFLAGS="-D warnings" cargo test --workspace
  • cargo clippy --all-targets --all-features -- -D warnings
  • cargo clippy --workspace --lib --bins --examples -- -D warnings -D clippy::unwrap_used -D clippy::expect_used
  • cargo clippy --workspace --all-targets --all-features -- -D warnings -W clippy::pedantic
  • dotnet format dotnet/Surge.slnx --verify-no-changes
  • dotnet test dotnet/Surge.slnx --configuration Release

Notes

Agent-authored PR: squash-merge this PR.

Write a validated metadata sidecar next to full packages produced by surge pack, then have surge push use it to record the original full archive compression settings in releases.yml.

This prevents full-only checkpoint releases from reaching storage with UNRECORDED encoding, which later made promotion unable to build channel-aware deltas.

Validation: ./scripts/sync-surge-core-vendor.sh --check; ./scripts/check-version-sync.sh; cargo fmt --all -- --check; RUSTFLAGS="-D warnings" cargo test --workspace; cargo clippy --all-targets --all-features -- -D warnings; cargo clippy --workspace --lib --bins --examples -- -D warnings -D clippy::unwrap_used -D clippy::expect_used; cargo clippy --workspace --all-targets --all-features -- -D warnings -W clippy::pedantic; dotnet format dotnet/Surge.slnx --verify-no-changes; dotnet test dotnet/Surge.slnx --configuration Release
Copy link
Copy Markdown
Contributor Author

peters commented May 6, 2026

Local smoke test completed against the real demoapp with the filesystem backend.

Scenario:

  • created a temporary filesystem-backed surge.yml
  • published demoapp artifacts for linux-x64 versions 1.0.0, 1.1.0, and 1.2.0
  • surge pack + surge push v1.0.0 to production
  • surge pack + surge push v1.1.0 to test, producing a delta
  • configured pack.delta.max_chain_length: 1
  • surge pack + surge push v1.2.0 to test, producing a full-only checkpoint
  • surge promote --version 1.2.0 --channel production

Observed result:

  • v1.2.0 logged Skipping delta package and publishing a checkpoint full
  • surge push loaded the full package encoding metadata sidecar
  • surge promote succeeded and rebuilt a production delta from v1.0.0: demoapp-1.2.0-linux-x64-from-1.0.0-delta.tar.zst
  • production channel ended at 1.2.0 (delta 3.1 KB via file-ops)
  • release index recorded full_compression_level: 3 and full_zstd_workers: 48 for the full-only v1.2.0 entry

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR preserves full-archive encoding metadata across surge pack -> surge push -> surge promote by writing a validated metadata sidecar next to full packages and having surge push persist the original compression settings into releases.yml (while remaining compatible with older package directories that have no sidecar).

Changes:

  • Write a .metadata.yml sidecar for each full package produced by surge pack.
  • Teach surge push to load/validate the sidecar and record full_compression_level / full_zstd_workers into the release index (fallback to UNRECORDED when absent).
  • Add/extend CLI tests to verify metadata persistence and sidecar creation.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
crates/surge-core/src/pack/builder.rs Adds sidecar schema/constants + metadata struct and writes full-package metadata next to archives.
crates/surge-cli/src/commands/push.rs Loads/validates sidecar metadata and records encoding settings into the release index; adds test coverage.
crates/surge-cli/src/commands/pack.rs Extends pack tests to assert metadata sidecar contents are emitted.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 381 to 403
/// Write built artifacts to `output_dir`.
pub fn write_artifacts_to(&self, output_dir: &Path) -> Result<Vec<PathBuf>> {
std::fs::create_dir_all(output_dir)?;

self.artifacts
let artifact_paths = self
.artifacts
.iter()
.map(|artifact| {
let path = output_dir.join(&artifact.filename);
write_file_atomic(&path, artifact.bytes())?;
Ok(path)
})
.collect()
.collect::<Result<Vec<_>>>()?;

for artifact in self.artifacts.iter().filter(|artifact| !artifact.is_delta) {
let metadata = PackageArtifactMetadata::for_full_artifact(&self.app_id, &self.version, &self.rid, artifact);
let metadata_yaml = serde_yaml::to_string(&metadata)?;
let metadata_path = output_dir.join(package_metadata_filename(&artifact.filename));
write_file_atomic(&metadata_path, metadata_yaml.as_bytes())?;
}

Ok(artifact_paths)
}
metadata_path.display()
)));
}

Comment on lines +1113 to +1123
let full_filename = format!("{app_id}-{version}-{rid}-full.tar.zst");
let metadata_path = packages_dir.join(package_metadata_filename(&full_filename));
let metadata: PackageArtifactMetadata =
serde_yaml::from_slice(&std::fs::read(&metadata_path).expect("package metadata should be readable"))
.expect("package metadata should parse");
assert_eq!(metadata.app_id, app_id);
assert_eq!(metadata.version, version);
assert_eq!(metadata.rid, rid);
assert_eq!(metadata.archive_filename, full_filename);
assert_eq!(metadata.full_compression_level, 3);
assert!(metadata.full_zstd_workers >= 0);
@peters peters marked this pull request as ready for review May 6, 2026 14:25
@peters peters merged commit f9ad051 into main May 6, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants