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
16 changes: 15 additions & 1 deletion .github/workflows/release-linux-repository-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,16 @@ jobs:
--expected-event workflow_dispatch --expected-head-branch "$RMUX_RELEASE_REF" \
"${mode[@]}" --max-attempts 1

- name: Preserve protected recovery metadata generator
shell: bash
run: |
set -euo pipefail
generator="$RUNNER_TEMP/rmux-recovery-generate-apt-repository.sh"
rm -f "$generator"
if test "$GITHUB_RUN_ID" != "$RMUX_RECEIPT_RUN_ID"; then
install -m 0755 scripts/generate-apt-repository.sh "$generator"
fi

- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
ref: ${{ inputs.expected_source_sha }}
Expand Down Expand Up @@ -157,6 +167,10 @@ jobs:
set -euo pipefail
root="$RUNNER_TEMP/rmux-linux-repository"
version="${RMUX_RELEASE_REF#v}"
apt_generator="scripts/generate-apt-repository.sh"
if test -x "$RUNNER_TEMP/rmux-recovery-generate-apt-repository.sh"; then
apt_generator="$RUNNER_TEMP/rmux-recovery-generate-apt-repository.sh"
fi
git clone --depth 1 --branch main \
https://github.com/Helvesec/rmux-packages.git "$root/history"
base="$(git -C "$root/history" rev-parse HEAD)"
Expand All @@ -171,7 +185,7 @@ jobs:
--rpm-signing-key "$RMUX_RPM_GPG_KEY" --current-version "$version" \
--apt-architecture amd64 --apt-architecture arm64 \
--rpm-architecture x86_64 --rpm-architecture aarch64
scripts/generate-apt-repository.sh \
"$apt_generator" \
--input-dir "$root/packages" --output-dir "$root/output/debian" \
--suite stable --component main --architecture amd64 --architecture arm64 \
--signing-key "$RMUX_APT_GPG_KEY"
Expand Down
7 changes: 7 additions & 0 deletions scripts/generate-apt-repository.sh
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,12 @@ for architecture in "${architectures[@]}"; do
} >> "$packages"
done
gzip -n -c "$packages" > "$packages.gz"
by_hash_dir="$binary_dir/by-hash/SHA256"
mkdir -p "$by_hash_dir"
for index in "$packages" "$packages.gz"; do
index_hash="$(hash_file sha256 "$index")"
cp "$index" "$by_hash_dir/$index_hash"
done
release_files+=("$packages" "$packages.gz")
done

Expand All @@ -182,6 +188,7 @@ Date: $date_utc
Architectures: ${architectures[*]}
Components: $component
Description: RMUX APT repository
Acquire-By-Hash: yes
MD5Sum:
$(release_hash_block md5 "$output_dir/dists/$suite" "${release_files[@]}")
SHA256:
Expand Down
4 changes: 4 additions & 0 deletions scripts/release/downstream_workflow_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ def _validate_reusable_workflow(path: Path, *, require_repository_guard: bool) -
raise ValueError(f"Linux repository recovery lost run mode {mode}")
if text.count('test "$GITHUB_REF" = "refs/heads/main"') != 1:
raise ValueError("Linux repository recovery is not bound to protected main")
if text.count("rmux-recovery-generate-apt-repository.sh") != 3:
raise ValueError(
"Linux repository recovery lost its protected APT generator"
)
elif "\n workflow_dispatch:" in text:
raise ValueError(f"{path.name} gained a mutation-capable dispatch trigger")
if "runs-on: self-hosted" in text or "\n - self-hosted" in text:
Expand Down
124 changes: 124 additions & 0 deletions tests/release_linux_repository_spec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#![cfg(unix)]

use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};

fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}

fn temp_dir(label: &str) -> PathBuf {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock after epoch")
.as_nanos();
std::env::temp_dir().join(format!("rmux-{label}-{}-{nonce}", std::process::id()))
}

#[cfg(unix)]
fn make_executable(path: &Path) {
use std::os::unix::fs::PermissionsExt;

let mut permissions = fs::metadata(path)
.expect("read tool metadata")
.permissions();
permissions.set_mode(0o755);
fs::set_permissions(path, permissions).expect("make tool executable");
}

fn sha256(path: &Path) -> String {
let output = Command::new("sha256sum")
.arg(path)
.output()
.expect("run sha256sum");
assert!(output.status.success());
String::from_utf8(output.stdout)
.expect("sha256sum output is UTF-8")
.split_whitespace()
.next()
.expect("sha256sum emitted a digest")
.to_owned()
}

#[test]
#[cfg(unix)]
fn apt_repository_publishes_sha256_by_hash_indexes() {
let root = temp_dir("apt-by-hash");
let input = root.join("input");
let output = root.join("output");
let tools = root.join("tools");
fs::create_dir_all(&input).expect("create input");
fs::create_dir_all(&tools).expect("create tools");
fs::write(input.join("rmux_0.9.1_amd64.deb"), b"amd64 package").expect("write amd64 package");
fs::write(input.join("rmux_0.9.1_arm64.deb"), b"arm64 package").expect("write arm64 package");

let dpkg_deb = tools.join("dpkg-deb");
fs::write(
&dpkg_deb,
r#"#!/bin/sh
set -eu
test "$1" = -f
case "$2" in
*_amd64.deb) architecture=amd64 ;;
*_arm64.deb) architecture=arm64 ;;
*) exit 64 ;;
esac
printf 'Package: rmux\nVersion: 0.9.1\nArchitecture: %s\n' "$architecture"
"#,
)
.expect("write dpkg-deb fixture");
make_executable(&dpkg_deb);

let path = std::env::join_paths(std::iter::once(tools.clone()).chain(std::env::split_paths(
&std::env::var_os("PATH").expect("PATH is defined"),
)))
.expect("compose PATH");
let result = Command::new(repo_root().join("scripts/generate-apt-repository.sh"))
.args(["--input-dir"])
.arg(&input)
.args(["--output-dir"])
.arg(&output)
.args([
"--suite",
"stable",
"--component",
"main",
"--architecture",
"amd64",
"--architecture",
"arm64",
])
.env("PATH", path)
.current_dir(repo_root())
.output()
.expect("generate APT repository");
assert!(
result.status.success(),
"{}",
String::from_utf8_lossy(&result.stderr)
);

let suite = output.join("dists/stable");
let release = fs::read_to_string(suite.join("Release")).expect("read Release");
assert!(release.contains("\nAcquire-By-Hash: yes\n"));
for architecture in ["amd64", "arm64"] {
let binary = suite.join(format!("main/binary-{architecture}"));
for name in ["Packages", "Packages.gz"] {
let index = binary.join(name);
let digest = sha256(&index);
let by_hash = binary.join("by-hash/SHA256").join(&digest);
assert_eq!(
fs::read(&by_hash).expect("read by-hash index"),
fs::read(&index).expect("read canonical index")
);
assert!(
release.contains(&format!(" main/binary-{architecture}/{name}\n")),
"Release does not bind {architecture}/{name}"
);
}
}

fs::remove_dir_all(root).expect("remove fixture");
}
Loading