Merge rotation-2 (7c9a939) into release-0.3.6: packaged config from files, --dl-both, logs --rotation; windows.yml keeps the G13 signed-inputs step after the packaged configuration step
This commit is contained in:
commit
0f6fc2ead5
16 changed files with 1176 additions and 124 deletions
43
.github/workflows/windows.yml
vendored
43
.github/workflows/windows.yml
vendored
|
|
@ -20,6 +20,16 @@
|
|||
# this checkout, all before anything is built from them (review round 4, R4.5.2, ledger G13). The verified
|
||||
# manifest, its signature and the runner's record go up as the igneum-windows-inputs artifact, which
|
||||
# packaging/windows/fetch-ci-artifacts.sh re-verifies on the Mac before it will sign an update manifest.
|
||||
# The Mac side of the loop is packaging/windows/fetch-ci-artifacts.sh (gh run download into the downloads folder).
|
||||
#
|
||||
# The packaged configuration (rotation phase 2, 5 October 2026; docs/plans/rotation-phase-2.md): the runner writes the
|
||||
# repository secrets to the same files the Mac keeps under ~/.config/igneum, and make-payload.sh picks them exactly as
|
||||
# on the Mac (packaging/mac/packaged-config.sh: a .next file wins when present).
|
||||
# LOG_INTAKE_KEY required: the intake key the payload ships (gh secret set LOG_INTAKE_KEY < ~/.config/igneum/log-intake-key)
|
||||
# LOG_INTAKE_KEY_NEXT optional, during a rotation: the next key; when set it is the one the payload ships
|
||||
# DL_TOKEN required: the folder the inputs come from, and the manifest folder when no DL_TOKEN_NEXT
|
||||
# DL_TOKEN_NEXT optional, during a rotation: the manifest folder the payload checks
|
||||
# After a rotation the owner sets LOG_INTAKE_KEY and DL_TOKEN to the new values and deletes the two _NEXT secrets.
|
||||
name: windows-ci
|
||||
on:
|
||||
push:
|
||||
|
|
@ -116,23 +126,44 @@ jobs:
|
|||
cargo build --release --locked
|
||||
ls -la target/release/igneum-app.exe
|
||||
|
||||
- name: packaged configuration (the secrets as the files packaged-config.sh reads; values never echoed)
|
||||
shell: bash
|
||||
env:
|
||||
DL_TOKEN: ${{ secrets.DL_TOKEN }}
|
||||
DL_TOKEN_NEXT: ${{ secrets.DL_TOKEN_NEXT }}
|
||||
LOG_INTAKE_KEY: ${{ secrets.LOG_INTAKE_KEY }}
|
||||
LOG_INTAKE_KEY_NEXT: ${{ secrets.LOG_INTAKE_KEY_NEXT }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$HOME/.config/igneum"
|
||||
if [ -z "${DL_TOKEN:-}" ]; then
|
||||
echo "::error::the DL_TOKEN repository secret is not set. On the Mac: tr -d '[:space:]' < ~/.config/igneum/dl-token | gh secret set DL_TOKEN --repo igneum-network/igneum"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "${LOG_INTAKE_KEY:-}" ] && [ -z "${LOG_INTAKE_KEY_NEXT:-}" ]; then
|
||||
echo "::error::neither LOG_INTAKE_KEY nor LOG_INTAKE_KEY_NEXT is set; the payload would ship without an intake key. On the Mac: tr -d '[:space:]' < ~/.config/igneum/log-intake-key | gh secret set LOG_INTAKE_KEY --repo igneum-network/igneum"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s' "$DL_TOKEN" > "$HOME/.config/igneum/dl-token"
|
||||
[ -n "${DL_TOKEN_NEXT:-}" ] && printf '%s' "$DL_TOKEN_NEXT" > "$HOME/.config/igneum/dl-token.next"
|
||||
[ -n "${LOG_INTAKE_KEY:-}" ] && printf '%s' "$LOG_INTAKE_KEY" > "$HOME/.config/igneum/log-intake-key"
|
||||
[ -n "${LOG_INTAKE_KEY_NEXT:-}" ] && printf '%s' "$LOG_INTAKE_KEY_NEXT" > "$HOME/.config/igneum/log-intake-key.next"
|
||||
chmod 600 "$HOME"/.config/igneum/*
|
||||
echo "files: $(ls "$HOME/.config/igneum" | tr '\n' ' ')"
|
||||
bash packaging/mac/packaged-config.sh --test
|
||||
|
||||
- name: payload inputs (payload-inputs.zip from the downloads host, signature, hashes and node commit verified)
|
||||
shell: bash
|
||||
env:
|
||||
DL_TOKEN: ${{ secrets.DL_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${DL_TOKEN:-}" ]; then
|
||||
echo "::error::the DL_TOKEN repository secret is not set. On the Mac: tr -d '[:space:]' < ~/.config/igneum/dl-token | gh secret set DL_TOKEN --repo igneum-network/igneum"
|
||||
exit 1
|
||||
fi
|
||||
base="https://dl.igneum.network/dl/$DL_TOKEN"
|
||||
signer="app/igneum-app/target/release/igneum-ota-sign.exe"
|
||||
[ -x "$signer" ] || { echo "::error::$signer was not built by the engine step"; exit 1; }
|
||||
pin="packaging/windows/node-source.pin"
|
||||
[ -s "$pin" ] || { echo "::error::$pin is missing: push-inputs.sh writes it, commit it with the inputs push"; exit 1; }
|
||||
mkdir -p build/inputs "$HOME/.config/igneum"
|
||||
printf '%s' "$DL_TOKEN" > "$HOME/.config/igneum/dl-token" # make-payload.sh reads it for the update manifest URL
|
||||
mkdir -p build/inputs # ~/.config/igneum/dl-token was written by the packaged configuration step
|
||||
curl -fsSL --retry 3 -o build/payload-inputs.json "$base/payload-inputs.json"
|
||||
curl -fsSL --retry 3 -o build/payload-inputs.json.sig "$base/payload-inputs.json.sig"
|
||||
curl -fsSL --retry 3 -o build/payload-inputs.zip "$base/payload-inputs.zip"
|
||||
|
|
|
|||
|
|
@ -158,16 +158,100 @@ pub struct Packaged {
|
|||
/// height, 4 October 2026: `{"difficulty_v2_activation_daa": N}`). Absent or empty = no override file.
|
||||
#[serde(default)]
|
||||
pub node_override_params: Option<serde_json::Value>,
|
||||
/// Where the key and the manifest came from, for the log header: "packaged", "file <name>" or "none". Never
|
||||
/// serialised (the packaged file does not carry them; nothing sends this struct to the UI).
|
||||
#[serde(skip)]
|
||||
pub key_source: String,
|
||||
#[serde(skip)]
|
||||
pub manifest_source: String,
|
||||
}
|
||||
|
||||
/// The downloads host; the manifest of a folder is `<host>/dl/<token>/igneum-app-latest.json`
|
||||
/// (packaging/mac/packaged-config.sh builds the same URL).
|
||||
pub const DL_HOST: &str = "https://dl.igneum.network";
|
||||
/// The intake a key file points at when the packaged file names no intake (a developer run).
|
||||
pub const DEFAULT_INTAKE_URL: &str = "https://igneum-six.vercel.app/api/log";
|
||||
|
||||
/// The manifest URL for a downloads token; empty for an empty token.
|
||||
pub fn manifest_url_for_token(token: &str) -> String {
|
||||
let t = token.trim();
|
||||
if t.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("{DL_HOST}/dl/{t}/igneum-app-latest.json")
|
||||
}
|
||||
}
|
||||
|
||||
/// The downloads token inside a manifest URL of the standard shape, or None.
|
||||
pub fn token_of_manifest_url(url: &str) -> Option<&str> {
|
||||
let rest = url.strip_prefix(DL_HOST)?.strip_prefix("/dl/")?;
|
||||
let (token, file) = rest.split_once('/')?;
|
||||
(file == "igneum-app-latest.json" && !token.is_empty()).then_some(token)
|
||||
}
|
||||
|
||||
/// A secret from a file: trimmed, None when the file is missing or blank.
|
||||
pub fn read_secret_file(path: &Path) -> Option<String> {
|
||||
let t = std::fs::read_to_string(path).ok()?;
|
||||
let t = t.trim();
|
||||
(!t.is_empty()).then(|| t.to_string())
|
||||
}
|
||||
|
||||
/// The first 8 hex of sha256 over a value: what logs and the console show instead of the value
|
||||
/// (`tr -d '[:space:]' < file | shasum -a 256 | cut -c1-8` gives the same on the Mac).
|
||||
pub fn fingerprint8(value: &str) -> String {
|
||||
use sha2::Digest;
|
||||
crate::manifest::hex_encode(&sha2::Sha256::digest(value.as_bytes()))[..8].to_string()
|
||||
}
|
||||
|
||||
impl Packaged {
|
||||
pub fn load(candidates: &[PathBuf]) -> Packaged {
|
||||
for c in candidates {
|
||||
if let Some(p) = std::fs::read_to_string(c).ok().and_then(|t| serde_json::from_str::<Packaged>(&t).ok()) {
|
||||
if let Some(mut p) = std::fs::read_to_string(c).ok().and_then(|t| serde_json::from_str::<Packaged>(&t).ok()) {
|
||||
p.key_source = if p.log_intake_key.is_empty() { "none".into() } else { "packaged".into() };
|
||||
p.manifest_source = if p.update_manifest.is_empty() { "none".into() } else { "packaged".into() };
|
||||
return p;
|
||||
}
|
||||
}
|
||||
Packaged::default()
|
||||
Packaged { key_source: "none".into(), manifest_source: "none".into(), ..Packaged::default() }
|
||||
}
|
||||
|
||||
/// Rotation phase 2 (5 October 2026, docs/plans/rotation-phase-2.md): the same two variables the packagers honour
|
||||
/// (packaging/mac/packaged-config.sh) work on a running engine, so a developer run or a build that was packaged
|
||||
/// with the old values can report to the rotated intake and check the rotated folder without a repackage:
|
||||
/// IGNEUM_INTAKE_KEY_FILE names a file holding the key, IGNEUM_DL_TOKEN_FILE a file holding the downloads token.
|
||||
/// A variable that is unset, or names a missing or blank file, changes nothing.
|
||||
pub fn with_env_overrides(self) -> Packaged {
|
||||
let file = |k: &str| std::env::var(k).ok().filter(|v| !v.is_empty()).map(PathBuf::from);
|
||||
self.with_file_overrides(file("IGNEUM_INTAKE_KEY_FILE").as_deref(), file("IGNEUM_DL_TOKEN_FILE").as_deref())
|
||||
}
|
||||
|
||||
pub fn with_file_overrides(mut self, key_file: Option<&Path>, token_file: Option<&Path>) -> Packaged {
|
||||
let name = |p: &Path| p.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| p.display().to_string());
|
||||
if let Some(key) = key_file.and_then(read_secret_file) {
|
||||
self.log_intake_key = key;
|
||||
if self.log_intake_url.is_empty() {
|
||||
self.log_intake_url = DEFAULT_INTAKE_URL.into();
|
||||
}
|
||||
self.key_source = format!("file {}", name(key_file.unwrap()));
|
||||
}
|
||||
if let Some(token) = token_file.and_then(read_secret_file) {
|
||||
self.update_manifest = manifest_url_for_token(&token);
|
||||
self.manifest_source = format!("file {}", name(token_file.unwrap()));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// The log header line: the intake URL with the key's fingerprint and the manifest URL with the folder's
|
||||
/// fingerprint, each with its source; the values themselves never appear (the log is uploaded).
|
||||
pub fn describe(&self) -> String {
|
||||
let key = if self.log_intake_key.is_empty() { "no key".to_string() } else { format!("key {}", fingerprint8(&self.log_intake_key)) };
|
||||
let intake = if self.log_intake_url.is_empty() { "none".to_string() } else { self.log_intake_url.clone() };
|
||||
let (manifest, folder) = match token_of_manifest_url(&self.update_manifest) {
|
||||
Some(t) => (self.update_manifest.replace(t, "<token>"), format!("folder {}", fingerprint8(t))),
|
||||
None if self.update_manifest.is_empty() => ("none".to_string(), "no folder".to_string()),
|
||||
None => (self.update_manifest.clone(), "custom".to_string()),
|
||||
};
|
||||
format!("config: intake {intake} {key} ({}); manifest {manifest} {folder} ({})", self.key_source, self.manifest_source)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -242,6 +326,107 @@ impl Runtime {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn tmp(name: &str, content: &str) -> PathBuf {
|
||||
// tests run in parallel: every file name is unique to its call
|
||||
static N: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
|
||||
let n = N.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let d = std::env::temp_dir().join(format!("igneum-config-test-{}-{n}-{}", std::process::id(), name));
|
||||
std::fs::write(&d, content).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
fn packaged(key: &str, token: &str) -> Packaged {
|
||||
let json = format!(r#"{{"update_manifest":"{}","log_intake_url":"https://igneum-six.vercel.app/api/log","log_intake_key":"{}"}}"#, manifest_url_for_token(token), key);
|
||||
let p = tmp("packaged.json", &json);
|
||||
let out = Packaged::load(&[p.clone()]);
|
||||
let _ = std::fs::remove_file(p);
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_url_round_trips_through_the_token() {
|
||||
assert_eq!(manifest_url_for_token("abc123"), "https://dl.igneum.network/dl/abc123/igneum-app-latest.json");
|
||||
assert_eq!(manifest_url_for_token(" abc123\n"), "https://dl.igneum.network/dl/abc123/igneum-app-latest.json");
|
||||
assert_eq!(manifest_url_for_token(""), "");
|
||||
assert_eq!(token_of_manifest_url("https://dl.igneum.network/dl/abc123/igneum-app-latest.json"), Some("abc123"));
|
||||
assert_eq!(token_of_manifest_url("https://dl.igneum.network/dl/abc123/other.json"), None);
|
||||
assert_eq!(token_of_manifest_url("http://127.0.0.1:8080/dl/t/igneum-app-latest.json"), None);
|
||||
assert_eq!(token_of_manifest_url(""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secret_files_are_trimmed_and_blank_means_none() {
|
||||
let f = tmp("key", " thekey0123456789abcdef \n");
|
||||
assert_eq!(read_secret_file(&f).as_deref(), Some("thekey0123456789abcdef"));
|
||||
std::fs::write(&f, " \n").unwrap();
|
||||
assert_eq!(read_secret_file(&f), None);
|
||||
let _ = std::fs::remove_file(&f);
|
||||
assert_eq!(read_secret_file(Path::new("/nonexistent/igneum/key")), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fingerprint_matches_shasum() {
|
||||
// printf abc | shasum -a 256 | cut -c1-8
|
||||
assert_eq!(fingerprint8("abc"), "ba7816bf");
|
||||
assert_eq!(fingerprint8("").len(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_records_the_sources() {
|
||||
let p = packaged("oldkey0123456789abcdef", "oldtok");
|
||||
assert_eq!(p.key_source, "packaged");
|
||||
assert_eq!(p.manifest_source, "packaged");
|
||||
let none = Packaged::load(&[PathBuf::from("/nonexistent/igneum-app.json")]);
|
||||
assert_eq!(none.key_source, "none");
|
||||
assert_eq!(none.manifest_source, "none");
|
||||
assert!(none.update_manifest.is_empty() && none.log_intake_key.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_overrides_replace_the_key_and_the_folder() {
|
||||
let key = tmp("log-intake-key.next", "newkey0123456789abcdef\n");
|
||||
let tok = tmp("dl-token.next", "newtok\n");
|
||||
let p = packaged("oldkey0123456789abcdef", "oldtok").with_file_overrides(Some(&key), Some(&tok));
|
||||
assert_eq!(p.log_intake_key, "newkey0123456789abcdef");
|
||||
assert_eq!(p.update_manifest, "https://dl.igneum.network/dl/newtok/igneum-app-latest.json");
|
||||
assert_eq!(p.log_intake_url, "https://igneum-six.vercel.app/api/log");
|
||||
assert!(p.key_source.starts_with("file ") && p.key_source.ends_with("log-intake-key.next"), "{}", p.key_source);
|
||||
assert!(p.manifest_source.ends_with("dl-token.next"), "{}", p.manifest_source);
|
||||
// only the key: the folder stays packaged
|
||||
let p = packaged("oldkey0123456789abcdef", "oldtok").with_file_overrides(Some(&key), None);
|
||||
assert_eq!(p.update_manifest, "https://dl.igneum.network/dl/oldtok/igneum-app-latest.json");
|
||||
assert_eq!(p.manifest_source, "packaged");
|
||||
// a missing or blank file changes nothing
|
||||
let blank = tmp("blank", "\n");
|
||||
let p = packaged("oldkey0123456789abcdef", "oldtok").with_file_overrides(Some(&blank), Some(Path::new("/nonexistent/dl-token")));
|
||||
assert_eq!(p.log_intake_key, "oldkey0123456789abcdef");
|
||||
assert_eq!(p.update_manifest, "https://dl.igneum.network/dl/oldtok/igneum-app-latest.json");
|
||||
assert_eq!(p.key_source, "packaged");
|
||||
// a developer run with no packaged file at all: the key file brings the default intake
|
||||
let p = Packaged::load(&[PathBuf::from("/nonexistent/igneum-app.json")]).with_file_overrides(Some(&key), Some(&tok));
|
||||
assert_eq!(p.log_intake_url, DEFAULT_INTAKE_URL);
|
||||
assert_eq!(p.update_manifest, "https://dl.igneum.network/dl/newtok/igneum-app-latest.json");
|
||||
for f in [key, tok, blank] {
|
||||
let _ = std::fs::remove_file(f);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn describe_never_carries_the_values() {
|
||||
let p = packaged("oldkey0123456789abcdef", "oldtok");
|
||||
let d = p.describe();
|
||||
assert!(!d.contains("oldkey"), "{d}");
|
||||
assert!(!d.contains("oldtok"), "{d}");
|
||||
assert!(d.contains("<token>/igneum-app-latest.json"), "{d}");
|
||||
assert!(d.contains(&format!("key {}", fingerprint8("oldkey0123456789abcdef"))), "{d}");
|
||||
assert!(d.contains(&format!("folder {}", fingerprint8("oldtok"))), "{d}");
|
||||
assert!(d.contains("(packaged)"), "{d}");
|
||||
let none = Packaged::load(&[PathBuf::from("/nonexistent/igneum-app.json")]).describe();
|
||||
assert!(none.contains("no key") && none.contains("no folder") && none.contains("(none)"), "{none}");
|
||||
let custom = Packaged { update_manifest: "http://127.0.0.1:9/dl/t/igneum-app-latest.json".into(), ..Packaged::default() }.describe();
|
||||
assert!(custom.contains("custom"), "{custom}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packaged_carries_the_node_override_params() {
|
||||
let p: Packaged = serde_json::from_str(r#"{"update_manifest":"","node_override_params":{"difficulty_v2_activation_daa":123456}}"#).unwrap();
|
||||
|
|
|
|||
|
|
@ -593,6 +593,9 @@ impl Engine {
|
|||
let v = crate::detect::node_version(&self.bins.node);
|
||||
self.st().node.version = v.clone();
|
||||
self.shared.log(&self.shared.upload_header());
|
||||
// which intake and which downloads folder this build reports to and checks (fingerprints, never the values;
|
||||
// rotation phase 2 reads this line from every machine's upload: docs/plans/rotation-phase-2.md)
|
||||
self.shared.log(&self.shared.packaged.describe());
|
||||
// the prover service (proving v0): its own thread, idle until the setting is on
|
||||
crate::prover::start(self.shared.clone(), self.bins.dir.clone());
|
||||
self.shared.log(&format!("node binary: {} ({v})", self.bins.node.display()));
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ fn main() {
|
|||
}
|
||||
}
|
||||
}
|
||||
let packaged = config::Packaged::load(&candidates);
|
||||
let packaged = config::Packaged::load(&candidates).with_env_overrides();
|
||||
let settings = config::Settings::load(&runtime.app_dir.join("settings.json"));
|
||||
|
||||
// the per-launch token: 32 hex characters from the OS
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ v4 changes the chain's formats, so it starts fresh from genesis and every node m
|
|||
4. You extract `igneum-windows-v4.zip` to a fresh folder and start it.
|
||||
5. We watch the live page: blocks, then the first lock after the window fills.
|
||||
|
||||
Packages (all three rebuilt 08:56 BST with generator v2 and the 2/3 floor, hosted): `https://dl.igneum.network/dl/***DL-TOKEN-REMOVED***/igneum-windows-v4.zip` (your PC, node and miners), `igneum-node-windows-v4.zip`, and `Igneum-Miner-0.2.0.dmg` for Sam, which dials the seed node first. The proof run for your 5090: `igneum-prove-wsl2.zip`, same folder; needs a reboot for WSL2 and twenty minutes of setup.
|
||||
Packages (all three rebuilt 08:56 BST with generator v2 and the 2/3 floor, hosted): `https://dl.igneum.network/dl/<token>/igneum-windows-v4.zip` (your PC, node and miners), `igneum-node-windows-v4.zip`, and `Igneum-Miner-0.2.0.dmg` for Sam, which dials the seed node first. The proof run for your 5090: `igneum-prove-wsl2.zip`, same folder; needs a reboot for WSL2 and twenty minutes of setup.
|
||||
|
||||
## What went wrong tonight, plainly
|
||||
|
||||
|
|
|
|||
273
docs/plans/rotation-phase-2.md
Normal file
273
docs/plans/rotation-phase-2.md
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
# Rotation phase 2: the 0.3.6 handover, the deletion of the old folder and key, the fresh repository (5 October 2026)
|
||||
|
||||
Internal. Phase 1 (4 October, 19:25 UTC) generated the NEXT intake key and the NEXT downloads token into
|
||||
`~/.config/igneum/log-intake-key.next` and `~/.config/igneum/dl-token.next`, taught `site/api/log.mjs` to accept
|
||||
`LOG_INTAKE_KEY_NEXT` next to `LOG_INTAKE_KEY`, and staged a second downloads folder `dl/<new token>/` with the
|
||||
installers of the day. Phase 2 is this file: the 0.3.6 build carries the new values, every installed 0.3.5 is carried
|
||||
across while the old folder still serves, then the old folder and the old key die, and only then the history is
|
||||
rewritten into a fresh repository (owner's decision, 5 October 2026; `docs/plans/history-rewrite.md`, option B).
|
||||
|
||||
No value is written here. Each is named by its fingerprint, the first 8 hex of sha256 over the trimmed value
|
||||
(`tr -d '[:space:]' < ~/.config/igneum/<file> | shasum -a 256 | cut -c1-8`; the app logs the same 8 characters):
|
||||
|
||||
| Value | File | Fingerprint | Where it lives today |
|
||||
|---|---|---|---|
|
||||
| old intake key | `~/.config/igneum/log-intake-key` | `e2005de8` | every installed app's `igneum-app.json` (0.3.0 to 0.3.5); the site project's `LOG_INTAKE_KEY`; 6 tracked files until this branch, 8 commits of the history |
|
||||
| new intake key | `~/.config/igneum/log-intake-key.next` | `477bb0ef` | nowhere yet (the site accepts it once `LOG_INTAKE_KEY_NEXT` is set) |
|
||||
| old downloads token | `~/.config/igneum/dl-token` | `df66a82c` | every installed app's manifest URL; `dl/<old>/` holds every version 0.1.0 to 0.3.5, the jobs file, the CI inputs; the `DL_TOKEN` repository secret; 1 commit of the history (`docs/plans/morning-2026-10-04.md`, masked on this branch) |
|
||||
| new downloads token | `~/.config/igneum/dl-token.next` | `ed9c4d2e` | `dl/<new>/` with the 0.3.3 installers and the WSL2 zip only, no manifest, no jobs file, no CI inputs |
|
||||
|
||||
## 1. What this branch changes (`rotation-2`)
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `packaging/mac/packaged-config.sh` | no key literal any more. `IGNEUM_INTAKE_KEY_FILE` and `IGNEUM_DL_TOKEN_FILE` name the files; each defaults to the `.next` file when it exists, else the plain file. Prints file names, lengths and fingerprints, never values. `--test` runs its 23 checks on temporary files |
|
||||
| `packaging/windows/make-payload.sh` | sources `packaged-config.sh` and calls `write_packaged_config` (it used to `sed` the key out of that file) |
|
||||
| `.github/workflows/windows.yml` | a "packaged configuration" step writes the repository secrets `DL_TOKEN`, `DL_TOKEN_NEXT`, `LOG_INTAKE_KEY`, `LOG_INTAKE_KEY_NEXT` to the same files under `~/.config/igneum` on the runner, so `make-payload.sh` picks them exactly as on the Mac; `LOG_INTAKE_KEY` or `LOG_INTAKE_KEY_NEXT` is now required (the key no longer comes from the tree) |
|
||||
| `app/igneum-app/src/config.rs` | `Packaged::with_env_overrides()` honours the same two variables on a running engine (a developer run, or a package built with the old values); `describe()` is the new header line `config: intake <url> key <fp> (<source>); manifest <url with the token masked> folder <fp> (<source>)`; `fingerprint8`, `manifest_url_for_token`, `token_of_manifest_url`, `read_secret_file`; 6 new unit tests |
|
||||
| `app/igneum-app/src/main.rs`, `engine.rs` | the overrides applied at load; the `config:` line logged right after the `IGNEUM-APP` header at every engine start (so every upload carries it) |
|
||||
| `tools/ship-app.mjs` | `--dl-both`: a `mirror` step copies the version's files and the folder-level files into `dl/<dl-token.next>/`, the `manifest` step publishes a second manifest there (`--dest`, `--base-url`, carrying the first manifest's `override`, `tuning` and `min_supported_version`, compared field by field), one deploy, `verify` checks both folders; self-test covers the three helpers |
|
||||
| `tools/logs.mjs` | `--rotation`: every app machine's version and header fingerprints against the `.next` files, exit 1 while any machine is behind; `--self-test` |
|
||||
| `infra/gpu-bench/upload.sh`, `proving/windows-wsl2/prove-block.sh`, `prove-shard.sh`, `proto-cuda/windows-app/upload-log.bat`, `proto-cuda/windows-miner/upload-log.bat` | the key literal removed: environment (`IGNEUM_LOG_KEY` or `IGNEUM_INTAKE_KEY`, which the app's job runner already sets), `IGNEUM_INTAKE_KEY_FILE`, or `igneum-log-key.txt` next to the .bat; the tree carries neither value now (`git grep` of both reads 0 files) |
|
||||
| `tools/repo/fresh-repo.sh` | the history rewrite of `docs/plans/history-rewrite.md` section 2 as one script with the verification greps and the printed push commands (section 6 below) |
|
||||
| `docs/plans/history-rewrite.md` | brought over from `testnet-prep` unchanged, so this branch carries the plan it executes |
|
||||
|
||||
## 2. The handover, as designed
|
||||
|
||||
An installed app reads `igneum-app.json` next to its engine (macOS `Contents/Resources`, Windows the install folder):
|
||||
the manifest URL and the intake key. The OTA path replaces the whole bundle or runs the whole installer, and both
|
||||
carry a new `igneum-app.json`, so the values travel with the version. Nothing is cached in the app data folder.
|
||||
|
||||
| Step | 0.3.5 on a machine (old folder, old key) | 0.3.6 (new folder, new key) |
|
||||
|---|---|---|
|
||||
| hourly check | fetches `dl/<old>/igneum-app-latest.json`: 0.3.6 is there (published in BOTH folders), signed by the same key | fetches `dl/<new>/igneum-app-latest.json`: itself |
|
||||
| download | the URL inside the old folder's manifest, `dl/<old>/Igneum-Miner-0.3.6.dmg` or `-Setup-0.3.6.exe` (byte-identical to the new folder's copy) | nothing |
|
||||
| apply | the new bundle or installer brings `igneum-app.json` with the NEW manifest URL and the NEW key | |
|
||||
| after restart | reports to the intake with the new key (`LOG_INTAKE_KEY_NEXT` accepts it); its header reads `key 477bb0ef` and `folder ed9c4d2e`; next check hits the NEW folder | the same |
|
||||
| jobs | `igneum-jobs.json` is read next to the manifest, so the mirror step copies the jobs file and its signature into the new folder; any job published while both folders live goes to both (section 3d) | |
|
||||
|
||||
The window: every 0.3.5 machine must apply 0.3.6 before the old folder goes. Machines apply in their own minute of the
|
||||
hour and only when the node is synced, so the window is hours, not minutes. The old folder and the old key stay until
|
||||
`node tools/logs.mjs --rotation` reads 0 behind (section 4). Nothing is deleted on a schedule.
|
||||
|
||||
## 3. The publish, exactly
|
||||
|
||||
Everything below runs from the main checkout after this branch is merged to master. `DLSITE` is the downloads folder
|
||||
(`~/.config/igneum/dlsite-dir`), `OLD` and `NEW` the two tokens read from their files; neither is ever typed.
|
||||
|
||||
### 3a. Before the cut (by hand, once)
|
||||
|
||||
```
|
||||
# the site must accept both keys (names only are listed; the value is piped from the file)
|
||||
cd site && npx --yes vercel@latest --global-config ~/.config/igneum/vercel link --scope igneum --project igneum --yes
|
||||
npx --yes vercel@latest --global-config ~/.config/igneum/vercel env ls --scope igneum # LOG_INTAKE_KEY must be there; is LOG_INTAKE_KEY_NEXT?
|
||||
tr -d '[:space:]' < ~/.config/igneum/log-intake-key.next | npx --yes vercel@latest --global-config ~/.config/igneum/vercel env add LOG_INTAKE_KEY_NEXT production --scope igneum
|
||||
cd .. && git push origin master # or any production deploy: the function reads the variable at the next deploy
|
||||
# confirm: a POST with the NEXT key is accepted (200 with an id), the old one still is too
|
||||
curl -s -o /dev/null -w '%{http_code}\n' -X POST -H "x-igneum-key: $(tr -d '[:space:]' < ~/.config/igneum/log-intake-key.next)" -H 'Content-Type: application/json' -d '{"label":"rotation-check","machine":"mac","run_id":"rotation-check","lines":"next key accepted"}' https://igneum-six.vercel.app/api/log
|
||||
|
||||
# the Windows build on GitHub needs the same files (the runner writes the secrets to ~/.config/igneum; windows.yml)
|
||||
gh auth switch --user igneum-labs
|
||||
tr -d '[:space:]' < ~/.config/igneum/log-intake-key | gh secret set LOG_INTAKE_KEY --repo igneum-network/igneum
|
||||
tr -d '[:space:]' < ~/.config/igneum/log-intake-key.next | gh secret set LOG_INTAKE_KEY_NEXT --repo igneum-network/igneum
|
||||
tr -d '[:space:]' < ~/.config/igneum/dl-token.next | gh secret set DL_TOKEN_NEXT --repo igneum-network/igneum
|
||||
gh secret list --repo igneum-network/igneum # DL_TOKEN, DL_TOKEN_NEXT, LOG_INTAKE_KEY, LOG_INTAKE_KEY_NEXT
|
||||
|
||||
# the new folder exists and is empty of a manifest (the mirror step fills it)
|
||||
DLSITE="$(tr -d '[:space:]' < ~/.config/igneum/dlsite-dir)"; NEW="$(tr -d '[:space:]' < ~/.config/igneum/dl-token.next)"
|
||||
ls "$DLSITE/dl/$NEW"
|
||||
packaging/mac/packaged-config.sh --test # 23 checks
|
||||
```
|
||||
|
||||
### 3b. The cut: one command, both folders
|
||||
|
||||
```
|
||||
node tools/ship-app.mjs 0.3.6 --node <fork worktree> --notes "<one line>" --dl-both --dry-run # the plan, nothing written
|
||||
node tools/ship-app.mjs 0.3.6 --node <fork worktree> --notes "<one line>" --dl-both
|
||||
```
|
||||
|
||||
With `--dl-both` the steps are: preflight (also: `dl-token.next` present and different, the folder exists) > bump >
|
||||
inputs > commit and push (the Windows build starts; its payload step runs `packaged-config.sh --test` and packages
|
||||
the `.next` values because the `_NEXT` secrets are set) > ci > fetch (installer and zip into the OLD folder) > dmg
|
||||
(`build-dmg.sh` packages `igneum-app.json` from the `.next` files by default) > copy (DMG into the OLD folder) >
|
||||
mirror (DMG, installer, zip, `igneum-windows-ci.json`, `igneum-jobs.json` and `.sig`, `payload-inputs.{zip,json,sha256}`,
|
||||
`igneum-prove-wsl2.zip` into the NEW folder, sha256-checked) > manifest (section 3c, both) > deploy (one) > verify
|
||||
(both folders: HEAD and GET of every file, both manifests byte-identical to the local ones and verifying, every
|
||||
platform URL inside its own folder) > console.
|
||||
|
||||
The build itself prints which files it packaged, for example `intake key: ~/.config/igneum/log-intake-key.next (31 chars,
|
||||
fingerprint 477bb0ef)` and `manifest: ~/.config/igneum/dl-token.next (10 chars, fingerprint ed9c4d2e) -> https://dl.igneum.network/dl/<token>/igneum-app-latest.json`.
|
||||
A build that says `e2005de8` or `df66a82c` packaged the old values: stop, the `.next` files were not found.
|
||||
|
||||
### 3c. The same by hand with `packaging/ota/publish-manifest.sh` (what the manifest step runs)
|
||||
|
||||
`publish-manifest.sh` writes the OLD folder by default (it reads `~/.config/igneum/dl-token`); `--dest <folder>` and
|
||||
`--base-url <url>` aim it at the NEW folder. With `--dest` it carries `consensus.override`, `tuning` and
|
||||
`min_supported_version` over from the manifest already in THAT folder, which is none, so the second call must pass
|
||||
what the first manifest carries. `--deploy` is refused with `--dest`; one deploy of the whole folder follows.
|
||||
Run by hand, `publish-manifest.sh` prints the manifest it wrote, URLs included, so the terminal shows the token
|
||||
path (it always has); `ship-app.mjs` scrubs both tokens from every line, which is the reason to prefer 3b.
|
||||
|
||||
```
|
||||
DLSITE="$(tr -d '[:space:]' < ~/.config/igneum/dlsite-dir)"
|
||||
OLD="$(tr -d '[:space:]' < ~/.config/igneum/dl-token)"; NEW="$(tr -d '[:space:]' < ~/.config/igneum/dl-token.next)"
|
||||
V=0.3.6; NOTES="<one line>"
|
||||
|
||||
# 1. the OLD folder (default dest and base): the files are there from fetch and copy
|
||||
packaging/ota/publish-manifest.sh --version $V --notes "$NOTES" --no-deploy \
|
||||
--mac "$DLSITE/dl/$OLD/Igneum-Miner-$V.dmg" --win "$DLSITE/dl/$OLD/Igneum-Miner-Setup-$V.exe"
|
||||
|
||||
# 2. the NEW folder: the same bytes copied in, the same override, tuning and min_supported read from the first manifest
|
||||
cp "$DLSITE/dl/$OLD/Igneum-Miner-$V.dmg" "$DLSITE/dl/$OLD/Igneum-Miner-Setup-$V.exe" "$DLSITE/dl/$OLD/igneum-windows-app.zip" \
|
||||
"$DLSITE/dl/$OLD/igneum-windows-ci.json" "$DLSITE/dl/$OLD/igneum-jobs.json" "$DLSITE/dl/$OLD/igneum-jobs.json.sig" \
|
||||
"$DLSITE/dl/$OLD/payload-inputs.zip" "$DLSITE/dl/$OLD/payload-inputs.json" "$DLSITE/dl/$OLD/payload-inputs.sha256" \
|
||||
"$DLSITE/dl/$OLD/igneum-prove-wsl2.zip" "$DLSITE/dl/$NEW/"
|
||||
M="$DLSITE/dl/$OLD/igneum-app-latest.json"
|
||||
OVERRIDE="$(python3 -c 'import json,sys; o=json.load(open(sys.argv[1])).get("consensus",{}).get("override"); print(json.dumps(o,sort_keys=True,separators=(",",":")) if o else "")' "$M")"
|
||||
MINSUP="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("min_supported_version",""))' "$M")"
|
||||
python3 -c 'import json,sys; t=json.load(open(sys.argv[1])).get("tuning"); open(sys.argv[2],"w").write(json.dumps(t)) if t else None' "$M" /tmp/tuning-$V.json
|
||||
packaging/ota/publish-manifest.sh --version $V --notes "$NOTES" --no-deploy \
|
||||
--dest "$DLSITE/dl/$NEW" --base-url "https://dl.igneum.network/dl/$NEW" \
|
||||
--mac "$DLSITE/dl/$NEW/Igneum-Miner-$V.dmg" --win "$DLSITE/dl/$NEW/Igneum-Miner-Setup-$V.exe" \
|
||||
${OVERRIDE:+--override "$OVERRIDE"} ${MINSUP:+--min-supported "$MINSUP"} $([ -s /tmp/tuning-$V.json ] && echo --tuning /tmp/tuning-$V.json || echo --no-tuning)
|
||||
rm -f /tmp/tuning-$V.json
|
||||
# the two manifests must differ only in published_at and the folder inside the URLs
|
||||
diff <(python3 -c 'import json,sys; m=json.load(open(sys.argv[1])); m.pop("published_at"); print(json.dumps(m,sort_keys=True,indent=1).replace(sys.argv[2],"T"))' "$M" "$OLD") \
|
||||
<(python3 -c 'import json,sys; m=json.load(open(sys.argv[1])); m.pop("published_at"); print(json.dumps(m,sort_keys=True,indent=1).replace(sys.argv[2],"T"))' "$DLSITE/dl/$NEW/igneum-app-latest.json" "$NEW") && echo "same fields"
|
||||
|
||||
# 3. one deploy, then both live checks (each: reachable, byte-identical to the local file, signature verifies)
|
||||
(cd "$DLSITE" && npx --yes vercel@latest --global-config ~/.config/igneum/vercel deploy --prod --yes)
|
||||
packaging/ota/publish-manifest.sh --verify-only
|
||||
packaging/ota/publish-manifest.sh --verify-only --dest "$DLSITE/dl/$NEW" --base-url "https://dl.igneum.network/dl/$NEW"
|
||||
```
|
||||
|
||||
### 3d. Jobs while both folders live
|
||||
|
||||
`packaging/ota/publish-jobs.sh` writes `dl/<old>/igneum-jobs.json` by default and takes the same `--dest` and
|
||||
`--base-url`. Until the old folder is deleted, every `add` or `expire` is published twice, the second time with
|
||||
`--dest "$DLSITE/dl/$NEW" --base-url "https://dl.igneum.network/dl/$NEW"`, then one deploy. A job whose
|
||||
`zip_url` names the old folder keeps working until that folder goes; publish new jobs with URLs in the new folder.
|
||||
|
||||
## 4. Verification: every machine's header shows the new intake path
|
||||
|
||||
The engine logs two header lines at every start, and the restart after an OTA apply logs them again, so the latest
|
||||
upload of every machine carries them:
|
||||
|
||||
```
|
||||
IGNEUM-APP version=0.3.6 machine=<id8> platform=<os> node=<igneumd version>
|
||||
config: intake https://igneum-six.vercel.app/api/log key 477bb0ef (packaged); manifest https://dl.igneum.network/dl/<token>/igneum-app-latest.json folder ed9c4d2e (packaged)
|
||||
```
|
||||
|
||||
```
|
||||
node tools/logs.mjs --rotation # one row per app machine: version, key fp, folder fp, sources, state; exit 1 while any is behind
|
||||
node tools/logs.mjs <run_id> | grep -E 'IGNEUM-APP|config: intake' # one machine in full
|
||||
```
|
||||
|
||||
Done means: every row `moved` (key `477bb0ef`, folder `ed9c4d2e`, version 0.3.6), none `OLD`, and the `unknown` rows
|
||||
(a machine whose last upload predates this header, or a machine that has stopped for good) accounted for by name.
|
||||
Today the table shows 6 app machines (three `win-`, three `mac-`), all 0.3.5 or older, all `unknown` because 0.3.5
|
||||
has no `config:` line. The console's Machines tab (`relay/`) shows the versions the same way.
|
||||
|
||||
Also check, once, that the intake stores an upload under the new key from a real machine (the row's `last_received`
|
||||
moves after the restart), and that `node tools/logs.mjs` lists no new `rotation-check` rows beyond the one from 3a.
|
||||
|
||||
## 5. The deletion, after section 4 reads 0 behind
|
||||
|
||||
In this order, each step checked before the next:
|
||||
|
||||
```
|
||||
DLSITE="$(tr -d '[:space:]' < ~/.config/igneum/dlsite-dir)"
|
||||
OLD="$(tr -d '[:space:]' < ~/.config/igneum/dl-token)"; NEW="$(tr -d '[:space:]' < ~/.config/igneum/dl-token.next)"
|
||||
node tools/logs.mjs --rotation || { echo "machines still behind"; false; }
|
||||
|
||||
# 1. the old folder: gone from the downloads host (one deploy); the new one still serves
|
||||
mv "$DLSITE/dl/$OLD" "$HOME/igneum-dl-old-$(date -u +%Y%m%d)" # kept outside the site for a week, then rm -rf
|
||||
(cd "$DLSITE" && npx --yes vercel@latest --global-config ~/.config/igneum/vercel deploy --prod --yes)
|
||||
curl -s -o /dev/null -w '%{http_code}\n' "https://dl.igneum.network/dl/$OLD/igneum-app-latest.json" # 404
|
||||
packaging/ota/publish-manifest.sh --verify-only --dest "$DLSITE/dl/$NEW" --base-url "https://dl.igneum.network/dl/$NEW" # still live
|
||||
|
||||
# 2. the local files: the NEXT values become the plain ones (every script's default), the old ones kept dated
|
||||
mv ~/.config/igneum/log-intake-key ~/.config/igneum/log-intake-key.old-$(date -u +%Y%m%d)
|
||||
mv ~/.config/igneum/log-intake-key.next ~/.config/igneum/log-intake-key
|
||||
mv ~/.config/igneum/dl-token ~/.config/igneum/dl-token.old-$(date -u +%Y%m%d)
|
||||
mv ~/.config/igneum/dl-token.next ~/.config/igneum/dl-token
|
||||
packaging/ota/publish-manifest.sh --verify-only # now reads the new token by default: live, verified
|
||||
|
||||
# 3. the intake: the old key dropped (LOG_INTAKE_KEY becomes the new value, LOG_INTAKE_KEY_NEXT removed), redeployed
|
||||
cd site
|
||||
npx --yes vercel@latest --global-config ~/.config/igneum/vercel env rm LOG_INTAKE_KEY production --scope igneum --yes
|
||||
tr -d '[:space:]' < ~/.config/igneum/log-intake-key | npx --yes vercel@latest --global-config ~/.config/igneum/vercel env add LOG_INTAKE_KEY production --scope igneum
|
||||
npx --yes vercel@latest --global-config ~/.config/igneum/vercel env rm LOG_INTAKE_KEY_NEXT production --scope igneum --yes
|
||||
cd .. && git push origin master # or a production deploy
|
||||
# the old key is dead (401), the new one lives (200)
|
||||
curl -s -o /dev/null -w '%{http_code}\n' -X POST -H "x-igneum-key: $(tr -d '[:space:]' < ~/.config/igneum/log-intake-key.old-$(date -u +%Y%m%d))" -H 'Content-Type: application/json' -d '{"label":"rotation-check","machine":"mac","run_id":"rotation-check","lines":"old key must be refused"}' https://igneum-six.vercel.app/api/log
|
||||
curl -s -o /dev/null -w '%{http_code}\n' -X POST -H "x-igneum-key: $(tr -d '[:space:]' < ~/.config/igneum/log-intake-key)" -H 'Content-Type: application/json' -d '{"label":"rotation-check","machine":"mac","run_id":"rotation-check","lines":"new key accepted"}' https://igneum-six.vercel.app/api/log
|
||||
|
||||
# 4. the GitHub secrets: the plain names carry the new values, the _NEXT names go
|
||||
tr -d '[:space:]' < ~/.config/igneum/dl-token | gh secret set DL_TOKEN --repo igneum-network/igneum
|
||||
tr -d '[:space:]' < ~/.config/igneum/log-intake-key | gh secret set LOG_INTAKE_KEY --repo igneum-network/igneum
|
||||
gh secret delete DL_TOKEN_NEXT --repo igneum-network/igneum; gh secret delete LOG_INTAKE_KEY_NEXT --repo igneum-network/igneum
|
||||
|
||||
# 5. the app machines keep reporting (a last look, an hour later)
|
||||
node tools/logs.mjs --rotation
|
||||
```
|
||||
|
||||
The relay is not involved: since round 4 (X23) it has its own key (`~/.config/igneum/relay-key`, `RELAY_KEY`), and
|
||||
the intake key only reports. The old launcher packages under `proto-cuda/windows-app` and `windows-miner` (0.2.0)
|
||||
carried the old key in `upload-log.bat`; those machines are the `unknown` rows of section 4 and upload nothing after
|
||||
step 3, which is the intent.
|
||||
|
||||
## 6. The fresh repository, after section 5
|
||||
|
||||
The old values are dead values once section 5 is done; only then is the rewrite worth running. The owner's two
|
||||
settings come first: rename the GitHub login `igneum-labs` to a neutral handle (the numeric noreply id 337424239
|
||||
stays, so the rewritten author line is `<new> <337424239+<new>@users.noreply.github.com>`), and remove the second
|
||||
login from the organisation's owners. Then, from the main checkout with every agent frozen:
|
||||
|
||||
```
|
||||
gh pr list --repo igneum-network/igneum # must be empty
|
||||
git worktree list > ~/igneum-worktrees-$(date -u +%Y%m%d).txt
|
||||
IGNEUM_FILTER_REPO=<dir>/git_filter_repo.py tools/repo/fresh-repo.sh --new-login <new handle> --new-repo igneum-network/<name> \
|
||||
[--public-claude-md <scrubbed CLAUDE.md>] --work ~/igneum-rewrite
|
||||
```
|
||||
|
||||
The script clones `origin` afresh (mirror, no hardlinks), reads the personal identities and the second login from
|
||||
the history and the four secret values from `~/.config/igneum`, writes the rule files 0600 and removes them after
|
||||
the pass, runs the one `git-filter-repo` invocation of `docs/plans/history-rewrite.md` section 2 (drop the four
|
||||
internal files, replace the secrets and the names, mailmap both personal identities to the login, every offset to
|
||||
`+0000`, optionally the public `CLAUDE.md` in every commit), then demands zero for: secret lines in any blob,
|
||||
identity lines in any blob, identity lines in commit metadata, stamps not `+0000`, commits touching the dropped
|
||||
files, identities other than the login, the old login in any blob (with `--new-login`). It prints the push commands
|
||||
and runs none of them: `gh repo create igneum-network/<name> --private`, `git remote add origin` in the clone,
|
||||
`git push --mirror origin`, then the GitHub secrets (section 3a), the Vercel Git connection moved to the new
|
||||
repository, the old repository archived, the main checkout re-cloned and every worktree re-created from its
|
||||
rewritten branch.
|
||||
|
||||
### Dry run of 5 October 2026 (throwaway mirror clone of the main checkout under the session scratchpad, nothing pushed)
|
||||
|
||||
| Count | Before | After |
|
||||
|---|---|---|
|
||||
| commits (all refs) | 410 | 353 (57 commits that only touched the four dropped files are gone) |
|
||||
| refs | 49 | 29 (filter-repo drops the remote-tracking refs of the mirror) |
|
||||
| author and committer identities | 3 | 1 (`igneum-labs <337424239+[removed]>`) |
|
||||
| stamps not +0000 | 660 of 820 | 0 of 706 |
|
||||
| commits touching the four dropped files | 53 | 0 |
|
||||
| secret lines in any blob (old key, new key, old token, new token) | 21 | 0 |
|
||||
| identity lines in any blob (first name outside the login, surname, personal addresses, second login, the other businesses) | 1839 | 0 |
|
||||
| identity lines in commit metadata | 171 | 0 |
|
||||
| standing login lines in any blob | 94 | 61 (0 with `--new-login` after the rename) |
|
||||
| pass run time | | 67 s; 4 min with the clone and the greps |
|
||||
|
||||
The report sits next to the clone (`<work>/report.txt`, with `commit-map`, 411 lines). The throwaway clone was
|
||||
removed after the run; nothing left the Mac.
|
||||
|
||||
## 7. The order for the afternoon
|
||||
|
||||
1. Merge `rotation-2` into master (the Windows build on that push packages with the `_NEXT` secrets only when they
|
||||
exist: set them first, section 3a, or expect the payload step to fail on the missing `LOG_INTAKE_KEY`).
|
||||
2. Section 3a: the site's `LOG_INTAKE_KEY_NEXT`, the four repository secrets, the curl check.
|
||||
3. Section 3b: `--dry-run`, then the cut with `--dl-both`.
|
||||
4. Section 4 through the afternoon: `node tools/logs.mjs --rotation` until 0 behind (the slot rule means an hour or
|
||||
two for a synced fleet; a machine that is off waits for its owner).
|
||||
5. Section 5: the deletion, in order.
|
||||
6. The owner's two GitHub settings (login rename, one owner); then section 6, the fresh repository, from a frozen tree.
|
||||
|
|
@ -5,7 +5,14 @@
|
|||
# ./upload.sh <logfile> <label> [run_id]
|
||||
set -euo pipefail
|
||||
IGNEUM_LOG_URL="${IGNEUM_LOG_URL:-https://igneum-six.vercel.app/api/log}"
|
||||
IGNEUM_LOG_KEY="${IGNEUM_LOG_KEY:-***INTAKE-KEY-REMOVED***}"
|
||||
# the key: IGNEUM_LOG_KEY, else the file named by IGNEUM_INTAKE_KEY_FILE, else ~/.config/igneum/log-intake-key.next when
|
||||
# staged, else ~/.config/igneum/log-intake-key (rotation phase 2, 5 October 2026: no key literal in the repository)
|
||||
if [ -z "${IGNEUM_LOG_KEY:-}" ]; then
|
||||
kf="${IGNEUM_INTAKE_KEY_FILE:-}"
|
||||
[ -n "$kf" ] || { [ -f "$HOME/.config/igneum/log-intake-key.next" ] && kf="$HOME/.config/igneum/log-intake-key.next" || kf="$HOME/.config/igneum/log-intake-key"; }
|
||||
[ -f "$kf" ] && IGNEUM_LOG_KEY="$(tr -d '[:space:]' < "$kf")" || IGNEUM_LOG_KEY=""
|
||||
fi
|
||||
[ -n "$IGNEUM_LOG_KEY" ] || { echo "upload: no intake key (set IGNEUM_LOG_KEY or IGNEUM_INTAKE_KEY_FILE)"; exit 3; }
|
||||
[ $# -ge 2 ] || { echo "usage: upload.sh <logfile> <label> [run_id]"; exit 1; }
|
||||
file="$1"; label="$2"; run_id="${3:-${IGNEUM_RUN_ID:-$label-$(date -u +%Y%m%d-%H%M)}}"
|
||||
[ -f "$file" ] || { echo "upload: file not found: $file"; exit 2; }
|
||||
|
|
|
|||
|
|
@ -1,27 +1,70 @@
|
|||
#!/bin/bash
|
||||
# The configuration the packagers write next to the engine (igneum-app.json): the update manifest URL (the download
|
||||
# token is read from ~/.config/igneum/dl-token and never lives in the repo), the log intake (the key only authorises
|
||||
# log uploads and is meant to ship, as the 0.2.0 launchers did), the live page. Sourced by build-dmg.sh; the Windows
|
||||
# make-payload.sh reads LOG_KEY from this file.
|
||||
LOG_URL="https://igneum-six.vercel.app/api/log"
|
||||
LOG_KEY="***INTAKE-KEY-REMOVED***"
|
||||
# The configuration the packagers write next to the engine (igneum-app.json): the update manifest URL, the log intake,
|
||||
# the live page. Sourced by packaging/mac/build-dmg.sh and packaging/windows/make-payload.sh (on the Mac and on the
|
||||
# GitHub runner alike). Run on its own (`packaging/mac/packaged-config.sh --test`) it checks itself.
|
||||
#
|
||||
# No secret lives in this file (rotation phase 2, 5 October 2026: the intake key used to be a literal here and is in
|
||||
# eight commits of the history; docs/plans/rotation-phase-2.md and docs/plans/history-rewrite.md). Both values come
|
||||
# from files named by two environment variables, each defaulting to the NEXT value when one is staged:
|
||||
#
|
||||
# IGNEUM_INTAKE_KEY_FILE the log intake key (authorises log uploads only; it ships inside every package).
|
||||
# Default: ~/.config/igneum/log-intake-key.next when that file exists, else
|
||||
# ~/.config/igneum/log-intake-key.
|
||||
# IGNEUM_DL_TOKEN_FILE the downloads path token: the manifest is https://dl.igneum.network/dl/<token>/igneum-app-latest.json.
|
||||
# Default: ~/.config/igneum/dl-token.next when that file exists, else ~/.config/igneum/dl-token.
|
||||
#
|
||||
# So the 0.3.6 build carries the rotated key and checks the manifest in the NEW folder with no flag at all, while a
|
||||
# build that must target the old folder says so: IGNEUM_DL_TOKEN_FILE=~/.config/igneum/dl-token. When the rotation is
|
||||
# over, `mv log-intake-key.next log-intake-key` and `mv dl-token.next dl-token` make the defaults the plain files
|
||||
# again. A missing or empty token file disables the update check (the note says so); a missing or empty key file
|
||||
# disables log uploads. Values are never printed, only the file names and the values' lengths.
|
||||
LOG_URL="${IGNEUM_INTAKE_URL:-https://igneum-six.vercel.app/api/log}"
|
||||
LIVE_PAGE="https://igneum.network/live"
|
||||
DOWNLOAD_PAGE="https://igneum.network/#mine"
|
||||
TOKEN_FILE="$HOME/.config/igneum/dl-token"
|
||||
DL_HOST="https://dl.igneum.network"
|
||||
# Consensus parameters pinned into the package for the bundled node: igneum-app.json "node_override_params"; the engine
|
||||
# writes them to <app data>/override-params.json and starts igneumd with --override-params-file. Empty = no override
|
||||
# file, the node runs the network's defaults. Difficulty v2 (4 Oct 2026): the version that bundles igneumd v2 must
|
||||
# carry the devnet's activation height here, the same N as every other devnet node, before it is cut (Mac and CI alike:
|
||||
# make-payload.sh reads this line). Rule and order: docs/plans/difficulty-v2-rollout-devnet.md.
|
||||
# make-payload.sh sources this file). Rule and order: docs/plans/difficulty-v2-rollout-devnet.md.
|
||||
# Example: NODE_OVERRIDE_PARAMS='{"difficulty_v2_activation_daa": 123456}'
|
||||
NODE_OVERRIDE_PARAMS=''
|
||||
|
||||
# writes the JSON to $1
|
||||
# igneum_secret_file <env var name> <base name> -> the file to read: the variable when set, else <base>.next when it
|
||||
# exists, else <base>; IGNEUM_CONFIG_DIR (tests) replaces ~/.config/igneum
|
||||
igneum_secret_file() {
|
||||
local var="$1" base="$2" dir="${IGNEUM_CONFIG_DIR:-$HOME/.config/igneum}" set_value=""
|
||||
set_value="${!var:-}"
|
||||
if [ -n "$set_value" ]; then printf '%s' "$set_value"
|
||||
elif [ -f "$dir/$base.next" ]; then printf '%s' "$dir/$base.next"
|
||||
else printf '%s' "$dir/$base"
|
||||
fi
|
||||
}
|
||||
# igneum_read_trimmed <file> -> the file's content without whitespace, or nothing when the file is missing or blank
|
||||
igneum_read_trimmed() {
|
||||
[ -f "$1" ] && tr -d '[:space:]' < "$1" || true
|
||||
}
|
||||
# igneum_manifest_url <token> -> the manifest URL for that downloads folder; nothing for an empty token
|
||||
igneum_manifest_url() {
|
||||
[ -n "$1" ] && printf '%s/dl/%s/igneum-app-latest.json' "$DL_HOST" "$1" || true
|
||||
}
|
||||
# igneum_fingerprint <value> -> the first 8 hex of sha256 over the value, for logs and the app's header (never the value)
|
||||
igneum_fingerprint() {
|
||||
printf '%s' "$1" | { shasum -a 256 2>/dev/null || sha256sum; } | cut -c1-8
|
||||
}
|
||||
|
||||
# writes the JSON to $1; prints which files were used (names), the lengths and the fingerprints, never the values
|
||||
write_packaged_config() {
|
||||
local out="$1" token="" manifest=""
|
||||
[ -f "$TOKEN_FILE" ] && token="$(tr -d '[:space:]' < "$TOKEN_FILE")"
|
||||
[ -n "$token" ] && manifest="https://dl.igneum.network/dl/$token/igneum-app-latest.json"
|
||||
[ -n "$manifest" ] || echo "note: no $TOKEN_FILE; the update check is disabled in this build"
|
||||
local out="$1" token="" manifest="" key="" key_file="" token_file=""
|
||||
key_file="$(igneum_secret_file IGNEUM_INTAKE_KEY_FILE log-intake-key)"
|
||||
token_file="$(igneum_secret_file IGNEUM_DL_TOKEN_FILE dl-token)"
|
||||
key="$(igneum_read_trimmed "$key_file")"
|
||||
token="$(igneum_read_trimmed "$token_file")"
|
||||
manifest="$(igneum_manifest_url "$token")"
|
||||
if [ -n "$key" ]; then echo "intake key: $key_file (${#key} chars, fingerprint $(igneum_fingerprint "$key"))"
|
||||
else echo "note: no key in $key_file; log uploads are disabled in this build"; fi
|
||||
if [ -n "$manifest" ]; then echo "manifest: $token_file (${#token} chars, fingerprint $(igneum_fingerprint "$token")) -> ${manifest//$token/<token>}"
|
||||
else echo "note: no token in $token_file; the update check is disabled in this build"; fi
|
||||
local override_line=""
|
||||
[ -n "$NODE_OVERRIDE_PARAMS" ] && override_line=" \"node_override_params\": $NODE_OVERRIDE_PARAMS,"
|
||||
cat > "$out" <<JSON
|
||||
|
|
@ -29,9 +72,61 @@ write_packaged_config() {
|
|||
$override_line
|
||||
"update_manifest": "$manifest",
|
||||
"log_intake_url": "$LOG_URL",
|
||||
"log_intake_key": "$LOG_KEY",
|
||||
"log_intake_key": "$key",
|
||||
"live_page": "$LIVE_PAGE",
|
||||
"download_page": "$DOWNLOAD_PAGE"
|
||||
}
|
||||
JSON
|
||||
}
|
||||
|
||||
# ---- self-test: packaging/mac/packaged-config.sh --test (temporary files only; nothing under ~/.config is read) -----
|
||||
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
||||
set -euo pipefail
|
||||
[ "${1:-}" = "--test" ] || { echo "usage: $0 --test (otherwise source this file)" >&2; exit 2; }
|
||||
T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
|
||||
PY="$(command -v python3 || command -v python)" # the GitHub Windows runner's Git Bash may only have python
|
||||
fails=0
|
||||
check() { if [ "$2" = "$3" ]; then echo " ok $1"; else echo " FAIL $1: got '$2', want '$3'"; fails=$((fails + 1)); fi; }
|
||||
export IGNEUM_CONFIG_DIR="$T/cfg"; mkdir -p "$T/cfg"
|
||||
unset IGNEUM_INTAKE_KEY_FILE IGNEUM_DL_TOKEN_FILE
|
||||
# 1. nothing staged: the plain files
|
||||
check "default key file is the plain one" "$(igneum_secret_file IGNEUM_INTAKE_KEY_FILE log-intake-key)" "$T/cfg/log-intake-key"
|
||||
check "default token file is the plain one" "$(igneum_secret_file IGNEUM_DL_TOKEN_FILE dl-token)" "$T/cfg/dl-token"
|
||||
# 2. a .next file wins
|
||||
printf 'nextkeyvalue0123456789abcdef\n' > "$T/cfg/log-intake-key.next"
|
||||
printf 'plainkeyvalue0123456789abcdef\n' > "$T/cfg/log-intake-key"
|
||||
printf 'tok2new\n' > "$T/cfg/dl-token.next"
|
||||
printf 'tok1old\n' > "$T/cfg/dl-token"
|
||||
check ".next key file wins when present" "$(igneum_secret_file IGNEUM_INTAKE_KEY_FILE log-intake-key)" "$T/cfg/log-intake-key.next"
|
||||
check ".next token file wins when present" "$(igneum_secret_file IGNEUM_DL_TOKEN_FILE dl-token)" "$T/cfg/dl-token.next"
|
||||
# 3. the variable beats both
|
||||
check "the variable names the file" "$(IGNEUM_DL_TOKEN_FILE="$T/cfg/dl-token" igneum_secret_file IGNEUM_DL_TOKEN_FILE dl-token)" "$T/cfg/dl-token"
|
||||
# 4. reading trims; a missing file reads as nothing
|
||||
check "trimmed read" "$(igneum_read_trimmed "$T/cfg/dl-token.next")" "tok2new"
|
||||
check "missing file reads empty" "$(igneum_read_trimmed "$T/cfg/none")" ""
|
||||
check "manifest url" "$(igneum_manifest_url tok2new)" "$DL_HOST/dl/tok2new/igneum-app-latest.json"
|
||||
check "empty token gives no manifest" "$(igneum_manifest_url '')" ""
|
||||
check "fingerprint is 8 hex" "$(igneum_fingerprint abc | grep -cE '^[0-9a-f]{8}$')" "1"
|
||||
check "fingerprint of abc" "$(igneum_fingerprint abc)" "ba7816bf"
|
||||
# 5. the written JSON: defaults pick the .next pair; the values land; nothing printed carries them
|
||||
out="$(write_packaged_config "$T/a.json")"
|
||||
check "output names the .next key file" "$(printf '%s' "$out" | grep -c 'log-intake-key.next')" "1"
|
||||
check "output never carries the key" "$(printf '%s' "$out" | grep -c 'nextkeyvalue')" "0"
|
||||
check "output never carries the token" "$(printf '%s' "$out" | grep -c 'tok2new')" "0"
|
||||
check "json carries the .next key" "$("$PY" -c 'import json,sys; print(json.load(open(sys.argv[1]))["log_intake_key"])' "$T/a.json")" "nextkeyvalue0123456789abcdef"
|
||||
check "json manifest points at the .next folder" "$("$PY" -c 'import json,sys; print(json.load(open(sys.argv[1]))["update_manifest"])' "$T/a.json")" "$DL_HOST/dl/tok2new/igneum-app-latest.json"
|
||||
check "json has no override line" "$("$PY" -c 'import json,sys; print("node_override_params" in json.load(open(sys.argv[1])))' "$T/a.json")" "False"
|
||||
# 6. the variables aim a build at the old folder and the old key
|
||||
out="$(IGNEUM_INTAKE_KEY_FILE="$T/cfg/log-intake-key" IGNEUM_DL_TOKEN_FILE="$T/cfg/dl-token" write_packaged_config "$T/b.json")"
|
||||
check "old-folder build: manifest" "$("$PY" -c 'import json,sys; print(json.load(open(sys.argv[1]))["update_manifest"])' "$T/b.json")" "$DL_HOST/dl/tok1old/igneum-app-latest.json"
|
||||
check "old-folder build: key" "$("$PY" -c 'import json,sys; print(json.load(open(sys.argv[1]))["log_intake_key"])' "$T/b.json")" "plainkeyvalue0123456789abcdef"
|
||||
# 7. missing files: notes, empty fields, valid JSON
|
||||
out="$(IGNEUM_INTAKE_KEY_FILE="$T/cfg/nokey" IGNEUM_DL_TOKEN_FILE="$T/cfg/notoken" write_packaged_config "$T/c.json")"
|
||||
check "missing key noted" "$(printf '%s' "$out" | grep -c 'log uploads are disabled')" "1"
|
||||
check "missing token noted" "$(printf '%s' "$out" | grep -c 'update check is disabled')" "1"
|
||||
check "missing files give empty fields" "$("$PY" -c 'import json,sys; j=json.load(open(sys.argv[1])); print(j["update_manifest"]+"|"+j["log_intake_key"])' "$T/c.json")" "|"
|
||||
# 8. the override line
|
||||
NODE_OVERRIDE_PARAMS='{"difficulty_v2_activation_daa": 123456}' write_packaged_config "$T/d.json" >/dev/null
|
||||
check "override params land" "$("$PY" -c 'import json,sys; print(json.load(open(sys.argv[1]))["node_override_params"]["difficulty_v2_activation_daa"])' "$T/d.json")" "123456"
|
||||
if [ "$fails" = 0 ]; then echo "packaged-config: all checks passed"; else echo "packaged-config: $fails check(s) failed"; exit 1; fi
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@
|
|||
# agent when they exist (searched in a few places; IGNEUM_WORKERS_DIR overrides); without them
|
||||
# the engine builds the CUDA worker from proto-cuda\ on the PC (needs the CUDA Toolkit and MSVC)
|
||||
# proto-cuda\, proto-opencl\ the worker sources and build.bat for that fallback
|
||||
# igneum-app.json the update manifest URL (token from ~/.config/igneum/dl-token, never in the repo), the log intake
|
||||
# igneum-app.json the update manifest URL and the log intake key, from the files named by IGNEUM_DL_TOKEN_FILE and
|
||||
# IGNEUM_INTAKE_KEY_FILE (defaults: the .next files under ~/.config/igneum when staged, else the
|
||||
# plain ones; packaging/mac/packaged-config.sh, sourced here; never in the repo)
|
||||
# stop-igneum.ps1 what the installer runs before an upgrade and on uninstall
|
||||
# app\windows\ host.cpp, host.rc, version.h, BUILD-APP.bat (the window host is built on the PC or by CI)
|
||||
# Igneum Miner.exe the window host, when app/windows/dist/ holds one (BUILD-APP.bat ran before this script)
|
||||
|
|
@ -31,7 +33,8 @@ REL="${IGNEUM_WIN_RELEASE:-$ROOT/vendor/igneum-node/target-integration/x86_64-pc
|
|||
ENGINE="${IGNEUM_APP_EXE:-$ROOT/app/igneum-app/target/x86_64-pc-windows-gnu/release/igneum-app.exe}"
|
||||
MINGW=/opt/homebrew/opt/mingw-w64/toolchain-x86_64/x86_64-w64-mingw32
|
||||
STAGE="$HERE/igneum-windows-app"
|
||||
TOKEN_FILE="$HOME/.config/igneum/dl-token"
|
||||
# the packaged configuration (the manifest URL from the token file, the intake key from the key file, NODE_OVERRIDE_PARAMS)
|
||||
. "$ROOT/packaging/mac/packaged-config.sh"
|
||||
|
||||
[ -f "$ENGINE" ] || { echo "no $ENGINE: build it first (cd app/igneum-app && cargo build --release --target x86_64-pc-windows-gnu, see proto-cuda/windows-node/cross-build.sh for the environment)" >&2; exit 1; }
|
||||
[ -f "$REL/igneumd.exe" ] || { echo "no $REL/igneumd.exe; cross-compile the node first" >&2; exit 1; }
|
||||
|
|
@ -94,27 +97,9 @@ else echo "warning: no Linux igneum-prove-host/igneum-prove-export in $PROVE_LIN
|
|||
cp "$ROOT"/proving/windows-wsl2/*.sh "$ROOT"/proving/windows-wsl2/*.ps1 "$ROOT"/proving/windows-wsl2/*.bat "$ROOT/proving/windows-wsl2/README.txt" "$STAGE/wsl2/"
|
||||
cp "$ROOT"/proving/fixtures/*.json "$STAGE/wsl2/fixtures/"
|
||||
|
||||
# the packaged configuration: the download token stays out of the repo
|
||||
TOKEN=""
|
||||
[ -f "$TOKEN_FILE" ] && TOKEN="$(tr -d '[:space:]' < "$TOKEN_FILE")"
|
||||
MANIFEST=""
|
||||
[ -n "$TOKEN" ] && MANIFEST="https://dl.igneum.network/dl/$TOKEN/igneum-app-latest.json"
|
||||
[ -n "$MANIFEST" ] || echo "note: no $TOKEN_FILE; the update check is disabled in this build"
|
||||
LOG_KEY="$(sed -n 's/^LOG_KEY="\(.*\)"/\1/p' "$ROOT/packaging/mac/packaged-config.sh")"
|
||||
# the pinned consensus parameters (packaged-config.sh NODE_OVERRIDE_PARAMS, e.g. the difficulty v2 activation height)
|
||||
NODE_OVERRIDE_PARAMS="$(sed -n "s/^NODE_OVERRIDE_PARAMS='\(.*\)'/\1/p" "$ROOT/packaging/mac/packaged-config.sh")"
|
||||
OVERRIDE_LINE=""
|
||||
[ -n "$NODE_OVERRIDE_PARAMS" ] && OVERRIDE_LINE=" \"node_override_params\": $NODE_OVERRIDE_PARAMS,"
|
||||
cat > "$STAGE/igneum-app.json" <<JSON
|
||||
{
|
||||
$OVERRIDE_LINE
|
||||
"update_manifest": "$MANIFEST",
|
||||
"log_intake_url": "https://igneum-six.vercel.app/api/log",
|
||||
"log_intake_key": "$LOG_KEY",
|
||||
"live_page": "https://igneum.network/live",
|
||||
"download_page": "https://igneum.network/#mine"
|
||||
}
|
||||
JSON
|
||||
# the packaged configuration: the token and the key stay out of the repo (packaged-config.sh prints the file names and
|
||||
# the fingerprints, never the values)
|
||||
write_packaged_config "$STAGE/igneum-app.json"
|
||||
cat > "$STAGE/README.txt" <<TXT
|
||||
Igneum Miner $VERSION for Windows (payload). Devnet v4. Test network; nothing is bought or sold.
|
||||
Nobody from Igneum will ever ask for your seed.
|
||||
|
|
|
|||
|
|
@ -3,10 +3,16 @@ rem Igneum miner log uploader. Sends the last 256 KB of a log file to the Igneum
|
|||
rem so Claude on the Mac can read it (node tools/logs.mjs). Needs Windows 10 or 11 (curl.exe, PowerShell).
|
||||
rem Usage: upload-log.bat <logfile> <label> [run_id]
|
||||
rem run_id falls back to the IGNEUM_RUN_ID environment variable, then to <label>-<date>-<time>.
|
||||
rem The key below only authorises log uploads. It is meant to ship inside this package.
|
||||
rem The key only authorises log uploads. It is not in the repository (rotation phase 2, 5 October 2026): it comes from
|
||||
rem the IGNEUM_LOG_KEY environment variable, else from igneum-log-key.txt next to this script (one line; the packager
|
||||
rem writes it from the file IGNEUM_INTAKE_KEY_FILE names), else the upload is refused with exit 3.
|
||||
setlocal
|
||||
set "IGNEUM_LOG_URL=https://igneum-six.vercel.app/api/log"
|
||||
set "IGNEUM_LOG_KEY=***INTAKE-KEY-REMOVED***"
|
||||
if "%IGNEUM_LOG_KEY%"=="" if exist "%~dp0igneum-log-key.txt" set /p IGNEUM_LOG_KEY=<"%~dp0igneum-log-key.txt"
|
||||
if "%IGNEUM_LOG_KEY%"=="" (
|
||||
echo upload-log: no intake key: set IGNEUM_LOG_KEY or put igneum-log-key.txt next to this script
|
||||
exit /b 3
|
||||
)
|
||||
|
||||
if "%~1"=="" goto usage
|
||||
if "%~2"=="" goto usage
|
||||
|
|
|
|||
|
|
@ -1,41 +1,47 @@
|
|||
@echo off
|
||||
rem Igneum miner log uploader. Sends the last 256 KB of a log file to the Igneum log intake
|
||||
rem so Claude on the Mac can read it (node tools/logs.mjs). Needs Windows 10 or 11 (curl.exe, PowerShell).
|
||||
rem Usage: upload-log.bat <logfile> <label> [run_id]
|
||||
rem run_id falls back to the IGNEUM_RUN_ID environment variable, then to <label>-<date>-<time>.
|
||||
rem The key below only authorises log uploads. It is meant to ship inside this package.
|
||||
setlocal
|
||||
set "IGNEUM_LOG_URL=https://igneum-six.vercel.app/api/log"
|
||||
set "IGNEUM_LOG_KEY=***INTAKE-KEY-REMOVED***"
|
||||
|
||||
if "%~1"=="" goto usage
|
||||
if "%~2"=="" goto usage
|
||||
if not exist "%~1" (
|
||||
echo upload-log: file not found: %~1
|
||||
exit /b 2
|
||||
)
|
||||
set "LOGFILE=%~f1"
|
||||
set "LABEL=%~2"
|
||||
set "RUNID=%~3"
|
||||
if "%RUNID%"=="" set "RUNID=%IGNEUM_RUN_ID%"
|
||||
set "OUT=%TEMP%\igneum-upload-%RANDOM%.json"
|
||||
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference='Stop'; $fs=New-Object IO.FileStream($env:LOGFILE,[IO.FileMode]::Open,[IO.FileAccess]::Read,[IO.FileShare]::ReadWrite); $b=New-Object byte[] $fs.Length; [void]$fs.Read($b,0,$b.Length); $fs.Close(); $n=[Math]::Min($b.Length,262144); $t=[Text.Encoding]::UTF8.GetString($b,$b.Length-$n,$n); $r=$env:RUNID; if (-not $r) { $r=$env:LABEL + '-' + (Get-Date -Format 'yyyyMMdd-HHmm') }; $o=@{label=$env:LABEL;machine=$env:COMPUTERNAME;run_id=$r;lines=$t}; [IO.File]::WriteAllText($env:OUT,(ConvertTo-Json $o -Compress),(New-Object Text.UTF8Encoding $false)); Write-Host ('upload-log: run_id ' + $r + ', ' + $n + ' bytes')"
|
||||
if errorlevel 1 (
|
||||
echo upload-log: could not read or encode %LOGFILE%
|
||||
exit /b 3
|
||||
)
|
||||
|
||||
curl.exe -sS --max-time 60 -X POST "%IGNEUM_LOG_URL%" -H "Content-Type: application/json" -H "x-igneum-key: %IGNEUM_LOG_KEY%" --data-binary "@%OUT%"
|
||||
set "RC=%ERRORLEVEL%"
|
||||
echo.
|
||||
del "%OUT%" >nul 2>&1
|
||||
if not "%RC%"=="0" (
|
||||
echo upload-log: curl failed with code %RC%
|
||||
exit /b %RC%
|
||||
)
|
||||
exit /b 0
|
||||
|
||||
:usage
|
||||
echo Usage: upload-log.bat ^<logfile^> ^<label^> [run_id]
|
||||
exit /b 1
|
||||
@echo off
|
||||
rem Igneum miner log uploader. Sends the last 256 KB of a log file to the Igneum log intake
|
||||
rem so Claude on the Mac can read it (node tools/logs.mjs). Needs Windows 10 or 11 (curl.exe, PowerShell).
|
||||
rem Usage: upload-log.bat <logfile> <label> [run_id]
|
||||
rem run_id falls back to the IGNEUM_RUN_ID environment variable, then to <label>-<date>-<time>.
|
||||
rem The key only authorises log uploads. It is not in the repository (rotation phase 2, 5 October 2026): it comes from
|
||||
rem the IGNEUM_LOG_KEY environment variable, else from igneum-log-key.txt next to this script (one line; the packager
|
||||
rem writes it from the file IGNEUM_INTAKE_KEY_FILE names), else the upload is refused with exit 3.
|
||||
setlocal
|
||||
set "IGNEUM_LOG_URL=https://igneum-six.vercel.app/api/log"
|
||||
if "%IGNEUM_LOG_KEY%"=="" if exist "%~dp0igneum-log-key.txt" set /p IGNEUM_LOG_KEY=<"%~dp0igneum-log-key.txt"
|
||||
if "%IGNEUM_LOG_KEY%"=="" (
|
||||
echo upload-log: no intake key: set IGNEUM_LOG_KEY or put igneum-log-key.txt next to this script
|
||||
exit /b 3
|
||||
)
|
||||
|
||||
if "%~1"=="" goto usage
|
||||
if "%~2"=="" goto usage
|
||||
if not exist "%~1" (
|
||||
echo upload-log: file not found: %~1
|
||||
exit /b 2
|
||||
)
|
||||
set "LOGFILE=%~f1"
|
||||
set "LABEL=%~2"
|
||||
set "RUNID=%~3"
|
||||
if "%RUNID%"=="" set "RUNID=%IGNEUM_RUN_ID%"
|
||||
set "OUT=%TEMP%\igneum-upload-%RANDOM%.json"
|
||||
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference='Stop'; $fs=New-Object IO.FileStream($env:LOGFILE,[IO.FileMode]::Open,[IO.FileAccess]::Read,[IO.FileShare]::ReadWrite); $b=New-Object byte[] $fs.Length; [void]$fs.Read($b,0,$b.Length); $fs.Close(); $n=[Math]::Min($b.Length,262144); $t=[Text.Encoding]::UTF8.GetString($b,$b.Length-$n,$n); $r=$env:RUNID; if (-not $r) { $r=$env:LABEL + '-' + (Get-Date -Format 'yyyyMMdd-HHmm') }; $o=@{label=$env:LABEL;machine=$env:COMPUTERNAME;run_id=$r;lines=$t}; [IO.File]::WriteAllText($env:OUT,(ConvertTo-Json $o -Compress),(New-Object Text.UTF8Encoding $false)); Write-Host ('upload-log: run_id ' + $r + ', ' + $n + ' bytes')"
|
||||
if errorlevel 1 (
|
||||
echo upload-log: could not read or encode %LOGFILE%
|
||||
exit /b 3
|
||||
)
|
||||
|
||||
curl.exe -sS --max-time 60 -X POST "%IGNEUM_LOG_URL%" -H "Content-Type: application/json" -H "x-igneum-key: %IGNEUM_LOG_KEY%" --data-binary "@%OUT%"
|
||||
set "RC=%ERRORLEVEL%"
|
||||
echo.
|
||||
del "%OUT%" >nul 2>&1
|
||||
if not "%RC%"=="0" (
|
||||
echo upload-log: curl failed with code %RC%
|
||||
exit /b %RC%
|
||||
)
|
||||
exit /b 0
|
||||
|
||||
:usage
|
||||
echo Usage: upload-log.bat ^<logfile^> ^<label^> [run_id]
|
||||
exit /b 1
|
||||
|
|
|
|||
|
|
@ -20,13 +20,22 @@ nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader 2>
|
|||
echo "cpu: $(nproc) cores, ram: $(free -g | awk '/Mem:/ {print $2}') GB visible to WSL"
|
||||
|
||||
upload() {
|
||||
# Last 256 KB of the log to the intake, same URL and key as upload-log.bat (log uploads only).
|
||||
# Last 256 KB of the log to the intake (log uploads only); key and URL from the environment, see below.
|
||||
python3 - "$LOG" "$RUN_ID" <<'PY'
|
||||
import json, socket, sys, urllib.request
|
||||
import json, os, socket, sys, urllib.request
|
||||
path, run_id = sys.argv[1], sys.argv[2]
|
||||
# the key and the intake come from the environment (the app's job runner sets IGNEUM_INTAKE_KEY and IGNEUM_INTAKE_URL;
|
||||
# by hand: export them, or IGNEUM_INTAKE_KEY_FILE naming a file); no key literal lives in the repository
|
||||
key = os.environ.get("IGNEUM_INTAKE_KEY", "").strip()
|
||||
if not key and os.environ.get("IGNEUM_INTAKE_KEY_FILE"):
|
||||
try: key = open(os.environ["IGNEUM_INTAKE_KEY_FILE"]).read().strip()
|
||||
except OSError: key = ""
|
||||
if not key:
|
||||
print("upload skipped: no IGNEUM_INTAKE_KEY in the environment"); sys.exit(0)
|
||||
url = os.environ.get("IGNEUM_INTAKE_URL", "").strip() or "https://igneum-six.vercel.app/api/log"
|
||||
data = open(path, 'rb').read()[-262144:].decode('utf-8', 'replace')
|
||||
body = json.dumps({"label": "prove-" + socket.gethostname(), "machine": socket.gethostname(), "run_id": run_id, "lines": data}).encode()
|
||||
req = urllib.request.Request("https://igneum-six.vercel.app/api/log", data=body, headers={"Content-Type": "application/json", "x-igneum-key": "***INTAKE-KEY-REMOVED***"})
|
||||
req = urllib.request.Request(url, data=body, headers={"Content-Type": "application/json", "x-igneum-key": key})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
print("upload:", r.status, r.read()[:200].decode('utf-8', 'replace'))
|
||||
|
|
|
|||
|
|
@ -21,13 +21,22 @@ nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader 2>
|
|||
echo "cpu: $(nproc) cores, ram: $(free -g | awk '/Mem:/ {print $2}') GB visible to WSL"
|
||||
|
||||
upload() {
|
||||
# Last 256 KB of the log to the intake, same URL and key as upload-log.bat (log uploads only).
|
||||
# Last 256 KB of the log to the intake (log uploads only); key and URL from the environment, see below.
|
||||
python3 - "$LOG" "$RUN_ID" <<'PY'
|
||||
import json, socket, sys, urllib.request
|
||||
import json, os, socket, sys, urllib.request
|
||||
path, run_id = sys.argv[1], sys.argv[2]
|
||||
# the key and the intake come from the environment (the app's job runner sets IGNEUM_INTAKE_KEY and IGNEUM_INTAKE_URL;
|
||||
# by hand: export them, or IGNEUM_INTAKE_KEY_FILE naming a file); no key literal lives in the repository
|
||||
key = os.environ.get("IGNEUM_INTAKE_KEY", "").strip()
|
||||
if not key and os.environ.get("IGNEUM_INTAKE_KEY_FILE"):
|
||||
try: key = open(os.environ["IGNEUM_INTAKE_KEY_FILE"]).read().strip()
|
||||
except OSError: key = ""
|
||||
if not key:
|
||||
print("upload skipped: no IGNEUM_INTAKE_KEY in the environment"); sys.exit(0)
|
||||
url = os.environ.get("IGNEUM_INTAKE_URL", "").strip() or "https://igneum-six.vercel.app/api/log"
|
||||
data = open(path, 'rb').read()[-262144:].decode('utf-8', 'replace')
|
||||
body = json.dumps({"label": "shards-" + socket.gethostname(), "machine": socket.gethostname(), "run_id": run_id, "lines": data}).encode()
|
||||
req = urllib.request.Request("https://igneum-six.vercel.app/api/log", data=body, headers={"Content-Type": "application/json", "x-igneum-key": "***INTAKE-KEY-REMOVED***"})
|
||||
req = urllib.request.Request(url, data=body, headers={"Content-Type": "application/json", "x-igneum-key": key})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
print("upload:", r.status, r.read()[:200].decode('utf-8', 'replace'))
|
||||
|
|
|
|||
|
|
@ -3,9 +3,58 @@
|
|||
// node tools/logs.mjs list runs: label, machine, run_id, last received, total bytes
|
||||
// node tools/logs.mjs <run_id> print the latest upload for that run
|
||||
// node tools/logs.mjs <run_id> --all print every upload for that run, oldest first
|
||||
// node tools/logs.mjs --rotation rotation phase 2 (docs/plans/rotation-phase-2.md): per app machine (labels mac-*,
|
||||
// win-*), the version and the "config:" header line of its latest upload (the
|
||||
// intake key's fingerprint and the downloads folder's fingerprint), against the
|
||||
// fingerprints of ~/.config/igneum/log-intake-key.next and dl-token.next; exit 1
|
||||
// while any machine still reports with the old values
|
||||
// node tools/logs.mjs --self-test the header parser on sample lines
|
||||
// Reads DATABASE_URL from ~/.config/igneum/env. No dependencies: Neon HTTP SQL over fetch.
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
// the two header lines the engine logs at every start (app/igneum-app/src/engine.rs run(), config.rs describe()):
|
||||
// IGNEUM-APP version=0.3.6 machine=1ccfe586 platform=windows node=...
|
||||
// config: intake https://.../api/log key 477bb0ef (packaged); manifest https://.../dl/<token>/igneum-app-latest.json folder ed9c4d2e (packaged)
|
||||
// The LAST occurrence wins (the restart after an OTA apply logs them again). Fields missing from the upload read ''.
|
||||
export function parseRotation(lines) {
|
||||
const out = { version: '', keyFp: '', keySource: '', folderFp: '', manifestSource: '' };
|
||||
for (const line of String(lines).split('\n')) {
|
||||
let m = /IGNEUM-APP version=(\S+)/.exec(line);
|
||||
if (m) out.version = m[1];
|
||||
m = /config: intake \S+ (?:key ([0-9a-f]{8})|no key) \((packaged|file [^)]+|none)\); manifest \S+ (?:folder ([0-9a-f]{8})|no folder|custom) \((packaged|file [^)]+|none)\)/.exec(line);
|
||||
if (m) { out.keyFp = m[1] || ''; out.keySource = m[2]; out.folderFp = m[3] || ''; out.manifestSource = m[4]; }
|
||||
}
|
||||
return out;
|
||||
}
|
||||
// the first 8 hex of sha256 over the trimmed file content; '' when the file is missing (the app's config::fingerprint8)
|
||||
export function fingerprintFile(path) {
|
||||
if (!existsSync(path)) return '';
|
||||
const v = readFileSync(path, 'utf8').trim();
|
||||
return v ? createHash('sha256').update(v).digest('hex').slice(0, 8) : '';
|
||||
}
|
||||
|
||||
if (process.argv[2] === '--self-test') {
|
||||
let fails = 0;
|
||||
const check = (name, ok, detail = '') => { console.log(` ${ok ? 'ok ' : 'FAIL'} ${name}${detail ? ': ' + detail : ''}`); if (!ok) fails++; };
|
||||
const sample = ['1 Igneum Miner 0.3.6 on pc (machine id 1ccfe586abcdef01), devnet (run win-1ccfe586-x)',
|
||||
'1 IGNEUM-APP version=0.3.5 machine=1ccfe586 platform=windows node=igneumd_0.3.5',
|
||||
'1 config: intake https://igneum-six.vercel.app/api/log key e2005de8 (packaged); manifest https://dl.igneum.network/dl/<token>/igneum-app-latest.json folder df66a82c (packaged)',
|
||||
'2 update: applied', '2 IGNEUM-APP version=0.3.6 machine=1ccfe586 platform=windows node=igneumd_0.3.6',
|
||||
'2 config: intake https://igneum-six.vercel.app/api/log key 477bb0ef (file log-intake-key.next); manifest https://dl.igneum.network/dl/<token>/igneum-app-latest.json folder ed9c4d2e (packaged)'].join('\n');
|
||||
const r = parseRotation(sample);
|
||||
check('the last header wins', r.version === '0.3.6' && r.keyFp === '477bb0ef' && r.folderFp === 'ed9c4d2e', JSON.stringify(r));
|
||||
check('sources are read', r.keySource === 'file log-intake-key.next' && r.manifestSource === 'packaged', JSON.stringify(r));
|
||||
const none = parseRotation('1 config: intake none no key (none); manifest none no folder (none)\n');
|
||||
check('a build without key or folder reads empty fingerprints', none.keyFp === '' && none.folderFp === '' && none.keySource === 'none', JSON.stringify(none));
|
||||
const custom = parseRotation('1 config: intake https://x/api/log key 01234567 (packaged); manifest http://127.0.0.1:9/dl/t/igneum-app-latest.json custom (packaged)\n');
|
||||
check('a custom manifest URL reads no folder', custom.keyFp === '01234567' && custom.folderFp === '', JSON.stringify(custom));
|
||||
check('an old upload without the config line reads version only', (() => { const o = parseRotation('1 IGNEUM-APP version=0.3.4 machine=a platform=mac node=x\n'); return o.version === '0.3.4' && o.keyFp === '' && o.keySource === ''; })());
|
||||
check('fingerprintFile of a missing file is empty', fingerprintFile('/nonexistent/igneum/key') === '');
|
||||
console.log(fails ? `${fails} check(s) failed` : 'all checks passed');
|
||||
process.exit(fails ? 1 : 0);
|
||||
}
|
||||
|
||||
process.stdout.on('error', e => { if (e.code === 'EPIPE') process.exit(0); throw e; });
|
||||
|
||||
|
|
@ -28,6 +77,30 @@ async function sql(query, params = []) {
|
|||
|
||||
const [runId, flag] = process.argv.slice(2);
|
||||
|
||||
if (runId === '--rotation') {
|
||||
const cfg = `${homedir()}/.config/igneum`;
|
||||
const want = { key: fingerprintFile(`${cfg}/log-intake-key.next`) || fingerprintFile(`${cfg}/log-intake-key`), folder: fingerprintFile(`${cfg}/dl-token.next`) || fingerprintFile(`${cfg}/dl-token`) };
|
||||
const old = { key: fingerprintFile(`${cfg}/log-intake-key`), folder: fingerprintFile(`${cfg}/dl-token`) };
|
||||
// the latest upload per app label (mac-<id8>, win-<id8>); the header lines sit in the first bytes, the config line
|
||||
// may repeat after an OTA restart, so the whole upload is parsed
|
||||
const rows = await sql(`
|
||||
SELECT DISTINCT ON (label) label, machine, run_id, received_at, lines
|
||||
FROM miner_logs WHERE label LIKE 'mac-%' OR label LIKE 'win-%'
|
||||
ORDER BY label, received_at DESC`);
|
||||
if (!rows.length) { console.log('No app uploads yet.'); process.exit(1); }
|
||||
let moved = 0, stale = 0;
|
||||
const table = rows.map(r => {
|
||||
const p = parseRotation(r.lines);
|
||||
const ok = p.keyFp === want.key && p.folderFp === want.folder;
|
||||
if (ok) moved++; else stale++;
|
||||
return { label: r.label, machine: r.machine, version: p.version || '?', key: p.keyFp || '-', folder: p.folderFp || '-', sources: [p.keySource, p.manifestSource].filter(Boolean).join(' / ') || '-', last_received: new Date(r.received_at).toISOString().replace('T', ' ').slice(0, 19) + ' UTC', state: ok ? 'moved' : p.keyFp === old.key || p.folderFp === old.folder ? 'OLD' : 'unknown' };
|
||||
}).sort((a, b) => a.state.localeCompare(b.state) || a.label.localeCompare(b.label));
|
||||
console.table(table);
|
||||
console.log(`expected: key ${want.key || '?'} folder ${want.folder || '?'} (from the .next files when they exist); old: key ${old.key || '?'} folder ${old.folder || '?'}`);
|
||||
console.log(`${moved} machine(s) on the new key and folder, ${stale} not yet; a machine silent for over a day is listed by its last upload`);
|
||||
process.exit(stale ? 1 : 0);
|
||||
}
|
||||
|
||||
if (!runId) {
|
||||
const rows = await sql(`
|
||||
SELECT run_id, max(label) AS label, max(machine) AS machine, count(*)::int AS uploads,
|
||||
|
|
|
|||
253
tools/repo/fresh-repo.sh
Executable file
253
tools/repo/fresh-repo.sh
Executable file
|
|
@ -0,0 +1,253 @@
|
|||
#!/usr/bin/env bash
|
||||
# The history rewrite of docs/plans/history-rewrite.md, section 2, as one script: a fresh mirror clone, one
|
||||
# git-filter-repo pass with the plan's rules, the greps that must read zero, and the commands (printed, never run)
|
||||
# that create the fresh repository under the organisation and push the rewritten refs there (the owner's decision of
|
||||
# 5 October 2026: option B, a fresh repository, never a force-push over the old one).
|
||||
#
|
||||
# tools/repo/fresh-repo.sh [--source <url|path>] [--work <dir>] [--new-repo <org/name>] [--new-login <login>]
|
||||
# [--public-claude-md <file>] [--clean]
|
||||
#
|
||||
# --source what to clone (default: this checkout's origin URL; a local path makes a throwaway dry run)
|
||||
# --work where the clone and the report go (default: a fresh directory under $TMPDIR); never inside a checkout
|
||||
# --new-repo the repository the printed commands create (default: igneum-network/igneum-core)
|
||||
# --new-login the renamed GitHub login (the owner renames it first; the numeric noreply id stays): every author
|
||||
# line and every file mention of the standing login is rewritten to it, and the identity grep then
|
||||
# demands zero hits for the old login too. Without it the standing login stays and is reported as such
|
||||
# --public-claude-md a scrubbed CLAUDE.md that replaces the file in EVERY commit (docs/fud-fixes.md section 5 step 2)
|
||||
# --clean remove the work directory at the end (the default keeps it: the push runs from that clone)
|
||||
#
|
||||
# Reads (never prints): ~/.config/igneum/log-intake-key, log-intake-key.next, dl-token, dl-token.next (those that
|
||||
# exist) for the secret rules and the secret grep; the personal identities and the second owner login are read from
|
||||
# the history itself (every author or committer that is not the standing login). The rule files are written 0600
|
||||
# in a 0700 directory and removed (rm -P) as soon as the pass has run. Nothing is pushed; nothing in --source changes.
|
||||
#
|
||||
# Needs git-filter-repo 2.38 or later: `git filter-repo` on PATH, or IGNEUM_FILTER_REPO=<path to git_filter_repo.py>
|
||||
# (pip: python3 -m pip install --target <dir> git-filter-repo). TZ is forced to UTC for everything this script runs.
|
||||
set -euo pipefail
|
||||
export TZ=UTC
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT="$(cd "$HERE/../.." && pwd)"
|
||||
STANDING_LOGIN="${IGNEUM_STANDING_LOGIN:-igneum-labs}"
|
||||
ORG="igneum-network"
|
||||
SOURCE="" WORK="" NEW_REPO="$ORG/igneum-core" NEW_LOGIN="" PUBLIC_CLAUDE="" CLEAN=0
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--source) SOURCE="$2"; shift 2 ;;
|
||||
--work) WORK="$2"; shift 2 ;;
|
||||
--new-repo) NEW_REPO="$2"; shift 2 ;;
|
||||
--new-login) NEW_LOGIN="$2"; shift 2 ;;
|
||||
--public-claude-md) PUBLIC_CLAUDE="$2"; shift 2 ;;
|
||||
--clean) CLEAN=1; shift ;;
|
||||
-h|--help) sed -n '2,24p' "$0"; exit 0 ;;
|
||||
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$SOURCE" ] || SOURCE="$(git -C "$ROOT" remote get-url origin)"
|
||||
[ -n "$WORK" ] || WORK="$(mktemp -d "${TMPDIR:-/tmp}/igneum-fresh-repo.XXXXXX")"
|
||||
case "$WORK" in /*) ;; *) WORK="$PWD/$WORK" ;; esac
|
||||
mkdir -p "$WORK"
|
||||
CLONE="$WORK/clone"
|
||||
[ ! -e "$CLONE" ] || { echo "$CLONE exists; the pass runs on a fresh clone only (remove it or give another --work)" >&2; exit 1; }
|
||||
if [ -n "$PUBLIC_CLAUDE" ]; then [ -f "$PUBLIC_CLAUDE" ] || { echo "no $PUBLIC_CLAUDE" >&2; exit 1; }; PUBLIC_CLAUDE="$(cd "$(dirname "$PUBLIC_CLAUDE")" && pwd)/$(basename "$PUBLIC_CLAUDE")"; fi
|
||||
case "$NEW_LOGIN" in *[!A-Za-z0-9-]*) echo "--new-login must be a GitHub login (letters, digits, hyphens)" >&2; exit 2 ;; esac
|
||||
[ "$NEW_LOGIN" != "$STANDING_LOGIN" ] || NEW_LOGIN=""
|
||||
|
||||
# the filter
|
||||
if [ -n "${IGNEUM_FILTER_REPO:-}" ]; then FILTER=(python3 "$IGNEUM_FILTER_REPO")
|
||||
elif git filter-repo --version >/dev/null 2>&1; then FILTER=(git filter-repo)
|
||||
elif python3 -c 'import git_filter_repo' 2>/dev/null; then FILTER=(python3 -m git_filter_repo)
|
||||
else echo "git-filter-repo is not installed: python3 -m pip install --target <dir> git-filter-repo, then IGNEUM_FILTER_REPO=<dir>/git_filter_repo.py" >&2; exit 1; fi
|
||||
command -v perl >/dev/null || { echo "perl is needed for the greps" >&2; exit 1; }
|
||||
|
||||
say() { printf '%s\n' "$*" | tee -a "$WORK/report.txt"; }
|
||||
: > "$WORK/report.txt"
|
||||
say "fresh-repo: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
say "source: $SOURCE"
|
||||
say "work: $WORK"
|
||||
say "filter: ${FILTER[*]} ($("${FILTER[@]}" --version 2>/dev/null | head -1 || echo '?'))"
|
||||
|
||||
# ---- 1. the fresh mirror clone ------------------------------------------------------------------------------------
|
||||
git clone --quiet --mirror --no-hardlinks "$SOURCE" "$CLONE"
|
||||
cd "$CLONE"
|
||||
|
||||
# ---- 2. the values, read at run time, never printed ------------------------------------------------------------------
|
||||
umask 077
|
||||
RULES="$WORK/rules"; mkdir -p "$RULES"; chmod 700 "$RULES"
|
||||
cleanup_rules() { if [ -d "$RULES" ]; then for f in "$RULES"/*; do [ -f "$f" ] && { rm -P "$f" 2>/dev/null || rm -f "$f"; }; done; rmdir "$RULES" 2>/dev/null || true; fi; }
|
||||
trap cleanup_rules EXIT
|
||||
|
||||
# the standing login's noreply address, from the history
|
||||
STANDING_EMAIL="$(git log --all --format='%ae%n%ce' | grep -E "^[0-9]+\+$STANDING_LOGIN@users\.noreply\.github\.com$" | sort -u | head -1 || true)"
|
||||
[ -n "$STANDING_EMAIL" ] || { echo "the history carries no commit by $STANDING_LOGIN (set IGNEUM_STANDING_LOGIN)" >&2; exit 1; }
|
||||
STANDING_ID="${STANDING_EMAIL%%+*}"
|
||||
if [ -n "$NEW_LOGIN" ]; then TARGET_LOGIN="$NEW_LOGIN"; else TARGET_LOGIN="$STANDING_LOGIN"; fi
|
||||
TARGET_EMAIL="$STANDING_ID+$TARGET_LOGIN@users.noreply.github.com"
|
||||
TARGET_IDENT="$TARGET_LOGIN <$TARGET_EMAIL>"
|
||||
|
||||
# every other identity: "name|email" pairs (author and committer)
|
||||
PERSONAL_PAIRS="$(git log --all --format='%an|%ae%n%cn|%ce' | grep -v "|$STANDING_EMAIL$" | sort -u || true)"
|
||||
PERSONAL_EMAILS="$(printf '%s\n' "$PERSONAL_PAIRS" | awk -F'|' 'NF==2{print $2}' | sort -u)"
|
||||
PERSONAL_NAMES="$(printf '%s\n' "$PERSONAL_PAIRS" | awk -F'|' 'NF==2{print $1}' | grep -v "^$STANDING_LOGIN$" | sort -u || true)"
|
||||
FIRST_NAMES="$(printf '%s\n' "$PERSONAL_NAMES" | awk 'NF>=1{print $1}' | sort -u)"
|
||||
LAST_NAMES="$(printf '%s\n' "$PERSONAL_NAMES" | awk 'NF>=2{print $NF}' | sort -u)"
|
||||
SECOND_LOGINS="$(printf '%s\n' "$PERSONAL_EMAILS" | sed -nE 's/^[0-9]+\+([A-Za-z0-9-]+)@users\.noreply\.github\.com$/\1/p' | grep -v "^$STANDING_LOGIN$" | sort -u || true)"
|
||||
# the other businesses named in the plan (brand names, not people)
|
||||
OTHER_BUSINESSES='[other-business]|[other-business]|[other-business]|[other-business]|[other-business]'
|
||||
# the secrets: whichever of the four files exist
|
||||
SECRET_FILES=(); for n in log-intake-key log-intake-key.next dl-token dl-token.next; do [ -f "$HOME/.config/igneum/$n" ] && SECRET_FILES+=("$HOME/.config/igneum/$n"); done
|
||||
|
||||
say "standing login: $STANDING_LOGIN (noreply id $STANDING_ID)${NEW_LOGIN:+ -> $NEW_LOGIN}"
|
||||
say "personal identities in the history: $(printf '%s\n' "$PERSONAL_PAIRS" | grep -c . || true) (names $(printf '%s\n' "$PERSONAL_NAMES" | grep -c . || true), addresses $(printf '%s\n' "$PERSONAL_EMAILS" | grep -c . || true), second owner logins $(printf '%s\n' "$SECOND_LOGINS" | grep -c . || true))"
|
||||
say "secret files for the rules: ${#SECRET_FILES[@]} of 4"
|
||||
|
||||
# the rule files
|
||||
REPLACE="$RULES/replace.txt"; MAILMAP="$RULES/mailmap"; IDENT="$RULES/identity.pl"; SECRETS="$RULES/secrets.pl"
|
||||
: > "$REPLACE"; : > "$MAILMAP"; : > "$IDENT"; : > "$SECRETS"
|
||||
for f in ${SECRET_FILES[@]+"${SECRET_FILES[@]}"}; do
|
||||
v="$(tr -d '[:space:]' < "$f")"; [ ${#v} -ge 8 ] || continue
|
||||
case "$(basename "$f")" in log-intake-key*) tag='***INTAKE-KEY-REMOVED***' ;; *) tag='***DL-TOKEN-REMOVED***' ;; esac
|
||||
printf 'literal:%s==>%s\n' "$v" "$tag" >> "$REPLACE"
|
||||
printf '%s\n' "$v" | sed 's/[][\\.*^$?+(){}|/]/\\&/g' >> "$SECRETS" # a regex for the scanner: metacharacters escaped (never \Q, which qr// does not expand from a variable)
|
||||
done
|
||||
while IFS='|' read -r name email; do
|
||||
[ -n "$email" ] || continue
|
||||
printf 'literal:%s <%s>==>%s\n' "$name" "$email" "$TARGET_IDENT" >> "$REPLACE"
|
||||
printf '%s <%s> <%s>\n' "$TARGET_LOGIN" "$TARGET_EMAIL" "$email" >> "$MAILMAP"
|
||||
done <<< "$PERSONAL_PAIRS"
|
||||
while IFS= read -r email; do
|
||||
[ -n "$email" ] || continue
|
||||
printf 'literal:%s==>[removed]\n' "$email" >> "$REPLACE"
|
||||
printf '%s\n' "$email" | sed 's/[][\\.*^$?+(){}|/]/\\&/g' >> "$IDENT"
|
||||
done <<< "$PERSONAL_EMAILS"
|
||||
if [ -n "$NEW_LOGIN" ]; then
|
||||
printf 'literal:%s==>%s\n' "$STANDING_EMAIL" "$TARGET_EMAIL" >> "$REPLACE"
|
||||
printf '%s <%s> <%s>\n' "$TARGET_LOGIN" "$TARGET_EMAIL" "$STANDING_EMAIL" >> "$MAILMAP"
|
||||
printf 'regex:\\b%s\\b==>%s\n' "$STANDING_LOGIN" "$NEW_LOGIN" >> "$REPLACE"
|
||||
printf '\\b%s\\b\n' "$STANDING_LOGIN" >> "$IDENT"
|
||||
fi
|
||||
while IFS= read -r first; do
|
||||
[ -n "$first" ] || continue
|
||||
printf 'regex:\\b%s%ss\\b==>the project lead%ss\n' "$first" "'" "'" >> "$REPLACE"
|
||||
while IFS= read -r last; do [ -n "$last" ] && printf 'regex:\\b%s\\s+%s\\b==>the project lead\n' "$first" "$last" >> "$REPLACE"; done <<< "$LAST_NAMES"
|
||||
printf 'regex:\\b%s\\b==>the project lead\n' "$first" >> "$REPLACE"
|
||||
done <<< "$FIRST_NAMES"
|
||||
while IFS= read -r last; do
|
||||
[ -n "$last" ] || continue
|
||||
printf 'regex:\\b%s\\b==>[removed]\n' "$last" >> "$REPLACE"
|
||||
printf '\\b%s\\b\n' "$last" >> "$IDENT"
|
||||
done <<< "$LAST_NAMES"
|
||||
while IFS= read -r first; do
|
||||
[ -n "$first" ] || continue
|
||||
# the lower-case user-name form (Windows and WSL paths, the browser profile), never the standing login's suffix
|
||||
printf 'regex:(?i)(?<!%s-)\\b%s\\b==>[user]\n' "${STANDING_LOGIN%%-*}" "$first" >> "$REPLACE"
|
||||
printf '(?<!%s-)\\b%s\\b\n' "${STANDING_LOGIN%%-*}" "$first" >> "$IDENT"
|
||||
done <<< "$FIRST_NAMES"
|
||||
while IFS= read -r login; do
|
||||
[ -n "$login" ] || continue
|
||||
printf 'regex:(?i)\\b%s\\b==>[second-owner-login]\n' "$login" >> "$REPLACE"
|
||||
printf '\\b%s\\b\n' "$login" >> "$IDENT"
|
||||
done <<< "$SECOND_LOGINS"
|
||||
printf 'regex:(?i)\\b(%s)\\b==>[other-business]\n' "$OTHER_BUSINESSES" >> "$REPLACE"
|
||||
printf '\\b(%s)\\b\n' "$OTHER_BUSINESSES" >> "$IDENT"
|
||||
say "rules: $(grep -c . "$REPLACE") replace lines, $(grep -c . "$MAILMAP") mailmap lines, $(grep -c . "$IDENT") identity patterns, $(grep -c . "$SECRETS") secret patterns (files under $RULES, removed at exit)"
|
||||
|
||||
# ---- 3. the counts, before and after ---------------------------------------------------------------------------------
|
||||
# every blob in the object store (reachable or not: before the pass the mirror holds everything, after it filter-repo's gc
|
||||
# has pruned), one count of matching lines over a pattern file (perl regexes, case-insensitive)
|
||||
scan_blobs() {
|
||||
git cat-file --batch-all-objects --batch-check='%(objectname) %(objecttype)' --unordered 2>/dev/null | awk '$2 == "blob" { print $1 }' \
|
||||
| git cat-file --batch 2>/dev/null \
|
||||
| perl -ne 'BEGIN { open(P, "<", shift) or die; @p = map { chomp; qr/$_/i } grep { /\S/ } <P>; $n = 0 } for my $p (@p) { if ($_ =~ $p) { $n++; last } } END { print "$n\n" }' "$1"
|
||||
}
|
||||
scan_meta() { git log --all --format='%an%n%ae%n%cn%n%ce%n%s%n%b' | perl -ne 'BEGIN { open(P, "<", shift) or die; @p = map { chomp; qr/$_/i } grep { /\S/ } <P>; $n = 0 } for my $p (@p) { if ($_ =~ $p) { $n++; last } } END { print "$n\n" }' "$1"; }
|
||||
DROPPED=(docs/fud-ledger.md docs/fud-fixes.md docs/review site/ledger.html)
|
||||
counts() { # <label>
|
||||
local label="$1"
|
||||
say ""
|
||||
say "[$label]"
|
||||
say " commits (all refs): $(git rev-list --all --count)"
|
||||
say " refs: $(git for-each-ref | wc -l | tr -d ' ')"
|
||||
say " author+committer identities: $(git log --all --format='%an <%ae>%n%cn <%ce>' | sort -u | wc -l | tr -d ' ')"
|
||||
say " stamps not +0000 (of $(git log --all --format='%ad%n%cd' --date=raw | wc -l | tr -d ' ')): $(git log --all --format='%ad%n%cd' --date=raw | grep -vc ' +0000$' || true)"
|
||||
say " commits touching the dropped files: $(git log --all --format=%H -- "${DROPPED[@]}" | sort -u | wc -l | tr -d ' ')"
|
||||
say " secret lines in any blob: $(scan_blobs "$SECRETS")"
|
||||
say " identity lines in any blob: $(scan_blobs "$IDENT")"
|
||||
say " identity lines in commit metadata: $(scan_meta "$IDENT")"
|
||||
say " standing login lines in any blob: $(printf '\\b%s\\b\n' "$STANDING_LOGIN" > "$RULES/login.pl"; scan_blobs "$RULES/login.pl")${NEW_LOGIN:+ (must be 0 with --new-login)}"
|
||||
}
|
||||
counts "before"
|
||||
|
||||
# ---- 4. the pass --------------------------------------------------------------------------------------------------
|
||||
say ""
|
||||
say "running: ${FILTER[*]} --force --invert-paths ${DROPPED[*]/#/--path } --replace-text <rules> --replace-message <rules> --mailmap <rules> --commit-callback <offsets to +0000>${PUBLIC_CLAUDE:+ --file-info-callback <CLAUDE.md from $PUBLIC_CLAUDE>}"
|
||||
PATH_ARGS=(); for p in "${DROPPED[@]}"; do PATH_ARGS+=(--path "$p"); done
|
||||
CALLBACK_ARGS=()
|
||||
if [ -n "$PUBLIC_CLAUDE" ]; then
|
||||
cat > "$RULES/file-info.py" <<PY
|
||||
if filename == b'CLAUDE.md':
|
||||
if 'claude' not in value.data:
|
||||
value.data['claude'] = value.insert_file_with_contents(open('$PUBLIC_CLAUDE', 'rb').read())
|
||||
return (filename, mode, value.data['claude'])
|
||||
return (filename, mode, blob_id)
|
||||
PY
|
||||
CALLBACK_ARGS+=(--file-info-callback "$RULES/file-info.py")
|
||||
fi
|
||||
T0=$(date +%s)
|
||||
"${FILTER[@]}" --force --quiet \
|
||||
--invert-paths "${PATH_ARGS[@]}" \
|
||||
--replace-text "$REPLACE" \
|
||||
--replace-message "$REPLACE" \
|
||||
--mailmap "$MAILMAP" \
|
||||
--commit-callback '
|
||||
for attr in ("author_date", "committer_date"):
|
||||
d = getattr(commit, attr); parts = d.split(b" ")
|
||||
if len(parts) == 2 and parts[1] != b"+0000":
|
||||
setattr(commit, attr, parts[0] + b" +0000")
|
||||
' ${CALLBACK_ARGS[@]+"${CALLBACK_ARGS[@]}"}
|
||||
say "pass done in $(( $(date +%s) - T0 )) s"
|
||||
[ -f .git/filter-repo/commit-map ] && cp .git/filter-repo/commit-map "$WORK/commit-map" && say "commit-map: $WORK/commit-map ($(grep -c . "$WORK/commit-map") lines; keep it with the private notes)"
|
||||
[ -f filter-repo/commit-map ] && cp filter-repo/commit-map "$WORK/commit-map" && say "commit-map: $WORK/commit-map ($(grep -c . "$WORK/commit-map") lines; keep it with the private notes)"
|
||||
|
||||
# ---- 5. the verification: every count that must read zero ------------------------------------------------------------
|
||||
counts "after"
|
||||
FAIL=0
|
||||
must_zero() { local what="$1" n="$2"; if [ "$n" != "0" ]; then say " FAIL $what: $n (must be 0)"; FAIL=1; else say " ok $what: 0"; fi; }
|
||||
say ""
|
||||
say "[verdict]"
|
||||
must_zero "secret lines in any blob" "$(scan_blobs "$SECRETS")"
|
||||
must_zero "identity lines in any blob" "$(scan_blobs "$IDENT")"
|
||||
must_zero "identity lines in commit metadata" "$(scan_meta "$IDENT")"
|
||||
must_zero "stamps not +0000" "$(git log --all --format='%ad%n%cd' --date=raw | grep -vc ' +0000$' || true)"
|
||||
must_zero "commits touching the dropped files" "$(git log --all --format=%H -- "${DROPPED[@]}" | sort -u | wc -l | tr -d ' ')"
|
||||
must_zero "identities other than $TARGET_IDENT" "$(git log --all --format='%an <%ae>%n%cn <%ce>' | sort -u | grep -vcF "$TARGET_IDENT" || true)"
|
||||
[ -n "$NEW_LOGIN" ] && must_zero "old login $STANDING_LOGIN in any blob" "$(scan_blobs "$RULES/login.pl")"
|
||||
if [ -n "$PUBLIC_CLAUDE" ]; then
|
||||
for ref in $(git for-each-ref --format='%(refname)' refs/heads | head -3); do
|
||||
if git cat-file -p "$ref:CLAUDE.md" 2>/dev/null | cmp -s - "$PUBLIC_CLAUDE"; then say " ok CLAUDE.md on $ref is the public text"; else say " FAIL CLAUDE.md on $ref is not the public text"; FAIL=1; fi
|
||||
done
|
||||
fi
|
||||
cleanup_rules; trap - EXIT
|
||||
if [ "$FAIL" = 1 ]; then say ""; say "NOT CLEAN: fix the rules and run again on a fresh clone (nothing was pushed)"; [ "$CLEAN" = 1 ] && rm -rf "$WORK"; exit 1; fi
|
||||
|
||||
# ---- 6. the commands that create the fresh repository and push (printed, never run) ---------------------------------
|
||||
say ""
|
||||
say "clean. The push, when the owner says so (option B of docs/plans/history-rewrite.md section 5; every line by hand):"
|
||||
say ""
|
||||
say " # 1. freeze: every agent has committed and pushed; gh pr list --repo $ORG/igneum is empty; git worktree list recorded"
|
||||
say " gh auth switch --user $TARGET_LOGIN && gh auth status"
|
||||
say " # 2. the fresh repository (private; the name is the owner's; igneum-core is the suggestion)"
|
||||
say " gh repo create $NEW_REPO --private --description 'Igneum: the GPU-mined zkEVM L1' --disable-wiki"
|
||||
say " # 3. push every rewritten ref from the clone (the pass removed its origin remote on purpose)"
|
||||
say " cd $CLONE"
|
||||
say " git remote add origin https://github.com/$NEW_REPO.git"
|
||||
say " git push --mirror origin"
|
||||
say " # 4. after the push, on GitHub: default branch master; Settings > Secrets: DL_TOKEN, DL_TOKEN_NEXT, LOG_INTAKE_KEY,"
|
||||
say " # LOG_INTAKE_KEY_NEXT (tr -d '[:space:]' < ~/.config/igneum/<file> | gh secret set <NAME> --repo $NEW_REPO);"
|
||||
say " # Vercel project igneum (team igneum): Git > disconnect $ORG/igneum, connect $NEW_REPO, production branch master;"
|
||||
say " # archive $ORG/igneum (Settings > Archive), keep it private; never delete it the same day"
|
||||
say " # 5. re-clone the main checkout from the new history and re-create every worktree from its rewritten branch:"
|
||||
say " cd ~/Projects && mv igneum igneum-old-history && git clone https://github.com/$NEW_REPO.git igneum"
|
||||
say " # for each worktree: git -C ~/Projects/igneum worktree add ../igneum-wt-<name> <branch>; vendor/ is copied back by hand (gitignored)"
|
||||
say " # 6. TZ=UTC in every shell that commits; tools/ci/identity-check.sh and the +0100 count stay the daily check"
|
||||
[ "$CLEAN" = 1 ] && { cd /; rm -rf "$WORK"; say "work directory removed (--clean)"; } || say "report: $WORK/report.txt; clone kept at $CLONE"
|
||||
exit 0
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
//
|
||||
// node tools/ship-app.mjs 0.3.4 --node <fork worktree> [--notes "..."] [--dry-run] [--from <step>]
|
||||
// [--skip-windows | --skip-mac] [--node-commit <sha>] [--win-release <dir>] [--mac-release <dir>]
|
||||
// [--min-supported 0.3.0] [--activation-height N --deadline-note "..."]
|
||||
// [--min-supported 0.3.0] [--activation-height N --deadline-note "..."] [--dl-both]
|
||||
// node tools/ship-app.mjs --check the six version files agree (exit 1 when they do not)
|
||||
// node tools/ship-app.mjs --self-test the bump, on a scratch copy of the version files
|
||||
//
|
||||
|
|
@ -18,11 +18,21 @@
|
|||
// fetch packaging/windows/fetch-ci-artifacts.sh <run>: the installer and the payload zip into the downloads folder
|
||||
// dmg packaging/mac/build-dmg.sh under tools/lock/with-lock.sh build (nice 19, 4 cargo jobs)
|
||||
// copy the DMG into the downloads folder
|
||||
// manifest packaging/ota/publish-manifest.sh --no-deploy: canonical JSON, signed, signature verified locally
|
||||
// mirror --dl-both only: the version's files and the folder-level files (jobs, payload inputs, CI record) into the
|
||||
// NEXT token folder, dl/<dl-token.next>/, so both folders carry the same bytes
|
||||
// manifest packaging/ota/publish-manifest.sh --no-deploy: canonical JSON, signed, signature verified locally; with
|
||||
// --dl-both a second manifest in the NEXT folder (--dest, --base-url) carrying the same override, tuning and
|
||||
// min_supported, checked field by field against the first
|
||||
// deploy the downloads folder with the Vercel CLI (one deploy carries the files and the manifest together)
|
||||
// verify HEAD and GET of the three files (size and sha256 against the local copies), the live manifest and its signature
|
||||
// verify HEAD and GET of the three files (size and sha256 against the local copies), the live manifest and its signature;
|
||||
// with --dl-both the same for the NEXT folder
|
||||
// console one console item (tools/console.mjs post --kind build) with version, sizes and hashes, then sync-dl
|
||||
//
|
||||
// --dl-both (rotation phase 2, 5 October 2026, docs/plans/rotation-phase-2.md): the downloads token is being rotated.
|
||||
// Installed apps check the OLD folder (dl-token); the new build checks the NEW one (dl-token.next, what
|
||||
// packaging/mac/packaged-config.sh packages by default while that file exists). The version is published in BOTH
|
||||
// folders so the old apps find the update and the new ones find their folder; one deploy, both verified.
|
||||
//
|
||||
// Secrets: ~/.config/igneum/dl-token, dlsite-dir, relay-token, relay-key, ota-signing-key, vercel/ are read by this
|
||||
// tool or by the scripts it calls and never printed; every output line is scrubbed of the tokens. State that is not a
|
||||
// secret (commit, run id, bump time) lives in ~/.cache/igneum/ship/<version>.json. gh auth switch --user igneum-labs runs
|
||||
|
|
@ -107,7 +117,7 @@ function checkVersionFiles(root) {
|
|||
}
|
||||
|
||||
// ---- output: every line scrubbed of the tokens -------------------------------------------------------------------
|
||||
const SECRETS = ['dl-token', 'relay-token', 'relay-key', 'log-intake-key'].map(cfg).filter(s => s.length >= 8);
|
||||
const SECRETS = ['dl-token', 'dl-token.next', 'relay-token', 'relay-key', 'log-intake-key', 'log-intake-key.next'].map(cfg).filter(s => s.length >= 8);
|
||||
const scrub = s => SECRETS.reduce((t, k) => t.split(k).join('<token>'), String(s));
|
||||
const say = (...a) => console.log(scrub(a.join(' ')));
|
||||
const fmtSize = n => n < 1024 ? `${n} B` : n < 1048576 ? `${(n / 1024).toFixed(1)} KB` : `${(n / 1048576).toFixed(1)} MB`;
|
||||
|
|
@ -154,7 +164,7 @@ for (let i = 0; i < argv.length; i++) {
|
|||
if (a.startsWith('--')) { const k = a.slice(2); const next = argv[i + 1]; if (next !== undefined && !next.startsWith('--')) { flags[k] = next; i++; } else flags[k] = true; }
|
||||
else pos.push(a);
|
||||
}
|
||||
const STEPS = ['preflight', 'bump', 'inputs', 'commit', 'ci', 'fetch', 'dmg', 'copy', 'manifest', 'deploy', 'verify', 'console'];
|
||||
const STEPS = ['preflight', 'bump', 'inputs', 'commit', 'ci', 'fetch', 'dmg', 'copy', 'mirror', 'manifest', 'deploy', 'verify', 'console'];
|
||||
|
||||
if (flags['self-test']) { process.exit(selfTest()); }
|
||||
if (flags.check) {
|
||||
|
|
@ -166,7 +176,7 @@ if (flags.check) {
|
|||
}
|
||||
|
||||
const VERSION = pos[0];
|
||||
if (!VERSION || !isVersion(VERSION)) { console.error('usage: node tools/ship-app.mjs <major.minor.patch> --node <fork worktree> [--notes "..."] [--dry-run] [--from <step>] [--skip-windows|--skip-mac]\n node tools/ship-app.mjs --check | --self-test'); process.exit(2); }
|
||||
if (!VERSION || !isVersion(VERSION)) { console.error('usage: node tools/ship-app.mjs <major.minor.patch> --node <fork worktree> [--notes "..."] [--dry-run] [--from <step>] [--skip-windows|--skip-mac] [--dl-both]\n node tools/ship-app.mjs --check | --self-test'); process.exit(2); }
|
||||
if (!flags.node) { console.error('--node <fork worktree> is required (the igneum-node worktree the node and miner were built from)'); process.exit(2); }
|
||||
if (flags.from && !STEPS.includes(flags.from)) { console.error(`--from must be one of: ${STEPS.join(', ')}`); process.exit(2); }
|
||||
if (flags['skip-windows'] && flags['skip-mac']) { console.error('--skip-windows and --skip-mac together leave nothing to ship'); process.exit(2); }
|
||||
|
|
@ -179,6 +189,14 @@ const TOKEN = cfg('dl-token');
|
|||
const DLSITE = process.env.IGNEUM_DLSITE || cfg('dlsite-dir');
|
||||
const DEST = DLSITE && TOKEN ? join(DLSITE, 'dl', TOKEN) : '';
|
||||
const BASE = `https://dl.igneum.network/dl/${TOKEN}`;
|
||||
// --dl-both: the NEXT folder, from ~/.config/igneum/dl-token.next
|
||||
const BOTH = !!flags['dl-both'];
|
||||
const TOKEN_NEXT = BOTH ? cfg('dl-token.next') : '';
|
||||
const DEST_NEXT = BOTH && DLSITE && TOKEN_NEXT ? join(DLSITE, 'dl', TOKEN_NEXT) : '';
|
||||
const BASE_NEXT = `https://dl.igneum.network/dl/${TOKEN_NEXT}`;
|
||||
// the folder-level files the apps and the CI read next to the manifest (jobs, the CI's inputs, the CI record, the
|
||||
// WSL2 prover zip): mirrored into the NEXT folder when present in the current one
|
||||
const FOLDER_FILES = ['igneum-jobs.json', 'igneum-jobs.json.sig', 'payload-inputs.zip', 'payload-inputs.json', 'payload-inputs.sha256', 'igneum-windows-ci.json', 'igneum-prove-wsl2.zip'];
|
||||
const DMG_NAME = `Igneum-Miner-${VERSION}.dmg`;
|
||||
const SETUP_NAME = `Igneum-Miner-Setup-${VERSION}.exe`;
|
||||
const ZIP_NAME = 'igneum-windows-app.zip';
|
||||
|
|
@ -193,7 +211,42 @@ const WIN_RELEASE = flags['win-release'] ? resolve(flags['win-release']) : first
|
|||
const MAC_RELEASE = flags['mac-release'] ? resolve(flags['mac-release']) : firstDir([join(NODE_DIR, 'target-integration', 'release'), join(NODE_DIR, 'target', 'release')], 'igneumd');
|
||||
const WIN_INPUTS = ['igneumd.exe', 'igneum-miner.exe'].map(n => join(WIN_RELEASE, n));
|
||||
const WORKERS = [join(ROOT, 'proto-cuda', 'nvrtc', 'igneum-worker-cuda.exe'), join(ROOT, 'proto-opencl', 'igneum-worker-opencl.exe')];
|
||||
const retryCmd = step => `node tools/ship-app.mjs ${VERSION} --node ${flags.node}${flags.notes ? ` --notes ${JSON.stringify(flags.notes)}` : ''}${WIN ? '' : ' --skip-windows'}${MAC ? '' : ' --skip-mac'}${flags['node-commit'] ? ` --node-commit ${flags['node-commit']}` : ''} --from ${step}`;
|
||||
const retryCmd = step => `node tools/ship-app.mjs ${VERSION} --node ${flags.node}${flags.notes ? ` --notes ${JSON.stringify(flags.notes)}` : ''}${WIN ? '' : ' --skip-windows'}${MAC ? '' : ' --skip-mac'}${flags['node-commit'] ? ` --node-commit ${flags['node-commit']}` : ''}${BOTH ? ' --dl-both' : ''} --from ${step}`;
|
||||
|
||||
// ---- --dl-both helpers (pure; the self-test runs them on scratch folders) -------------------------------------------
|
||||
// which of `names` must be copied from src to dst: 'copy' (missing or different bytes), 'same', or 'absent' (not in src)
|
||||
function mirrorPlan(src, dst, names) {
|
||||
return names.map(name => {
|
||||
const a = join(src, name), b = join(dst, name);
|
||||
if (!existsSync(a)) return { name, action: 'absent' };
|
||||
if (existsSync(b) && sha256(a) === sha256(b)) return { name, action: 'same' };
|
||||
return { name, action: 'copy' };
|
||||
});
|
||||
}
|
||||
// the arguments the second publish-manifest.sh call takes so the NEXT folder's manifest carries what the first one
|
||||
// carries (override, tuning, min_supported are otherwise carried over from the manifest already in THAT folder, which
|
||||
// is older or missing): [args, tuningFile|null]
|
||||
function secondManifestArgs(first, dest, base, tmpDir) {
|
||||
const args = ['--dest', dest, '--base-url', base];
|
||||
const o = first.consensus && first.consensus.override;
|
||||
if (o && typeof o === 'object' && Object.keys(o).length) args.push('--override', JSON.stringify(o));
|
||||
if (first.min_supported_version) args.push('--min-supported', String(first.min_supported_version));
|
||||
let tuningFile = null;
|
||||
if (first.tuning && typeof first.tuning === 'object' && first.tuning.cards) { tuningFile = join(tmpDir, 'tuning.json'); writeFileSync(tuningFile, JSON.stringify(first.tuning)); args.push('--tuning', tuningFile); }
|
||||
else args.push('--no-tuning');
|
||||
return [args, tuningFile];
|
||||
}
|
||||
// the two manifests must agree on everything except published_at and the folder in the URLs: the differences, [] when none
|
||||
function manifestDifferences(a, b, baseA, baseB) {
|
||||
const diffs = [];
|
||||
const norm = (m, base) => { const c = JSON.parse(JSON.stringify(m)); delete c.published_at; for (const e of Object.values(c.platforms || {})) if (typeof e.url === 'string') e.url = e.url.replace(base, '<base>'); return c; };
|
||||
const x = norm(a, baseA), y = norm(b, baseB);
|
||||
for (const k of new Set([...Object.keys(x), ...Object.keys(y)])) {
|
||||
const sx = JSON.stringify(x[k] === undefined ? null : x[k]), sy = JSON.stringify(y[k] === undefined ? null : y[k]);
|
||||
if (sx !== sy) diffs.push(`${k}: ${sx.slice(0, 80)} vs ${sy.slice(0, 80)}`);
|
||||
}
|
||||
return diffs;
|
||||
}
|
||||
const results = []; // the final table
|
||||
let dryProblems = 0; // a dry run lists preflight problems and goes on with the plan; its exit code says so
|
||||
const done = (step, result, detail = '') => { results.push([step, result, detail]); say(`[${step}] ${result}${detail ? ': ' + detail : ''}`); };
|
||||
|
|
@ -244,6 +297,12 @@ async function preflight() {
|
|||
for (const n of ['dl-token', 'dlsite-dir', 'ota-signing-key', 'ota-signing-key.pub', 'relay-token', 'relay-key']) if (!existsSync(join(CFG, n))) problems.push(`no ~/.config/igneum/${n}`);
|
||||
if (!existsSync(join(CFG, 'vercel'))) problems.push('no ~/.config/igneum/vercel (the Vercel login for the downloads host)');
|
||||
if (!DEST || !existsSync(DEST)) problems.push(`no downloads folder at <dlsite>/dl/<token> (~/.config/igneum/dlsite-dir says ${DLSITE || 'nothing'})`);
|
||||
if (BOTH) {
|
||||
if (!TOKEN_NEXT) problems.push('--dl-both needs ~/.config/igneum/dl-token.next (the next downloads token)');
|
||||
else if (TOKEN_NEXT === TOKEN) problems.push('--dl-both: dl-token.next equals dl-token; nothing to rotate');
|
||||
else if (!DEST_NEXT || !existsSync(DEST_NEXT)) problems.push('--dl-both: no folder at <dlsite>/dl/<dl-token.next>; mkdir it first (an empty folder is fine)');
|
||||
if (!existsSync(join(CFG, 'log-intake-key.next'))) notes.push('no ~/.config/igneum/log-intake-key.next: the packagers ship the current intake key (packaged-config.sh)');
|
||||
}
|
||||
// gh account (a read; the real steps switch before every call)
|
||||
const st = runSync('gh', ['auth', 'status']).out;
|
||||
const active = /Logged in to github\.com account (\S+) \(keyring\)\n\s+- Active account: true/.exec(st);
|
||||
|
|
@ -262,6 +321,7 @@ async function preflight() {
|
|||
['mac binaries', MAC ? ['igneumd', 'igneum-miner'].map(n => existsSync(join(MAC_RELEASE, n)) ? `${n} ${fmtSize(sizeOf(join(MAC_RELEASE, n)))}` : `${n} MISSING`).join(', ') : 'skipped'],
|
||||
['workers', WORKERS.map(w => existsSync(w) ? basename(w) : `${basename(w)} missing`).join(', ')],
|
||||
['downloads folder', DEST ? DEST.replace(TOKEN, '<token>') : 'none'],
|
||||
['next folder', BOTH ? (DEST_NEXT ? DEST_NEXT.replace(TOKEN_NEXT, '<token.next>') : 'MISSING') : 'not used (no --dl-both)'],
|
||||
['gh active', `${ghActive}${ghActive === GH_USER ? '' : ` (switched to ${GH_USER} before every call)`}`],
|
||||
['live inputs', live.inputs ? `node ${live.inputs.node_source_commit} built ${live.inputs.built_at}` : 'none'],
|
||||
['live ci', live.ci ? `${live.ci.installer} (${live.ci.run.split('/').pop()})` : 'none'],
|
||||
|
|
@ -402,19 +462,50 @@ async function copy() {
|
|||
done('copy', 'ok', `${DMG_NAME} ${fmtSize(sizeOf(dst))} copied`);
|
||||
}
|
||||
|
||||
async function mirror() {
|
||||
if (!BOTH) return done('mirror', 'skipped', 'no --dl-both');
|
||||
const names = [MAC && DMG_NAME, WIN && SETUP_NAME, WIN && ZIP_NAME, ...FOLDER_FILES].filter(Boolean);
|
||||
const plan = mirrorPlan(DEST, DEST_NEXT, names);
|
||||
const copies = plan.filter(p => p.action === 'copy'), same = plan.filter(p => p.action === 'same'), absent = plan.filter(p => p.action === 'absent');
|
||||
const summary = `${copies.length} to copy (${copies.map(p => p.name).join(', ') || 'none'}), ${same.length} same, ${absent.length} absent in the current folder${absent.length ? ` (${absent.map(p => p.name).join(', ')})` : ''}`;
|
||||
if (DRY) return done('mirror', 'would', `copy into dl/<token.next>/: ${summary}`);
|
||||
for (const name of [MAC && DMG_NAME, WIN && SETUP_NAME].filter(Boolean)) if (!existsSync(join(DEST, name))) throw new Error(`missing ${name} in the current folder; ${retryCmd(name.endsWith('.dmg') ? 'copy' : 'fetch')}`);
|
||||
for (const p of copies) copyFileSync(join(DEST, p.name), join(DEST_NEXT, p.name));
|
||||
const after = mirrorPlan(DEST, DEST_NEXT, names).filter(p => p.action === 'copy');
|
||||
if (after.length) throw new Error(`still differ after the copy: ${after.map(p => p.name).join(', ')}`);
|
||||
done('mirror', copies.length ? 'ok' : 'already', summary);
|
||||
}
|
||||
|
||||
async function manifest() {
|
||||
const args = ['--version', VERSION, '--notes', NOTES, '--no-deploy'];
|
||||
if (MAC) args.push('--mac', join(DEST, DMG_NAME));
|
||||
if (WIN) args.push('--win', join(DEST, SETUP_NAME));
|
||||
for (const k of ['min-supported', 'activation-height', 'deadline-note', 'channel']) if (flags[k]) args.push(`--${k}`, String(flags[k]));
|
||||
const cmd = `packaging/ota/publish-manifest.sh ${args.map(a => a.includes(' ') ? JSON.stringify(a) : a).join(' ')}`;
|
||||
if (DRY) return done('manifest', 'would', `run ${cmd.replace(TOKEN, '<token>')} (signs, verifies the signature; ${WIN && MAC ? 'both platforms' : WIN ? 'windows entry, mac carried over when the live manifest is this version' : 'mac entry, windows carried over when the live manifest is this version'})`);
|
||||
if (DRY) return done('manifest', 'would', `run ${cmd.replace(TOKEN, '<token>')} (signs, verifies the signature; ${WIN && MAC ? 'both platforms' : WIN ? 'windows entry, mac carried over when the live manifest is this version' : 'mac entry, windows carried over when the live manifest is this version'})${BOTH ? '; then the same for dl/<token.next>/ with --dest and --base-url, carrying this manifest\'s override, tuning and min_supported; the two compared field by field' : ''}`);
|
||||
for (const p of [MAC && join(DEST, DMG_NAME), WIN && join(DEST, SETUP_NAME)].filter(Boolean)) if (!existsSync(p)) throw new Error(`missing ${p.replace(TOKEN, '<token>')}; ${retryCmd(p.endsWith('.dmg') ? 'copy' : 'fetch')}`);
|
||||
const r = await run('bash', [join(ROOT, 'packaging', 'ota', 'publish-manifest.sh'), ...args]);
|
||||
if (r.code !== 0) throw new Error(`publish-manifest.sh exited ${r.code}; retry: ${cmd.replace(TOKEN, '<token>')}`);
|
||||
const m = JSON.parse(readFileSync(join(DEST, 'igneum-app-latest.json'), 'utf8'));
|
||||
if (m.version !== VERSION) throw new Error(`the written manifest says ${m.version}`);
|
||||
done('manifest', 'ok', `${VERSION} ${Object.keys(m.platforms).join('+')}, signed and verified locally`);
|
||||
if (!BOTH) return done('manifest', 'ok', `${VERSION} ${Object.keys(m.platforms).join('+')}, signed and verified locally`);
|
||||
// the NEXT folder: the same entries from its own copies, the same override, tuning and min_supported
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'igneum-ship-m-'));
|
||||
try {
|
||||
const [extra, tuningFile] = secondManifestArgs(m, DEST_NEXT, BASE_NEXT, tmp);
|
||||
const args2 = ['--version', VERSION, '--notes', NOTES, '--no-deploy', ...extra];
|
||||
if (MAC) args2.push('--mac', join(DEST_NEXT, DMG_NAME));
|
||||
if (WIN) args2.push('--win', join(DEST_NEXT, SETUP_NAME));
|
||||
for (const k of ['activation-height', 'deadline-note', 'channel']) if (flags[k]) args2.push(`--${k}`, String(flags[k]));
|
||||
const cmd2 = `packaging/ota/publish-manifest.sh ${args2.map(a => a.includes(' ') || a.startsWith('{') ? JSON.stringify(a) : a).join(' ')}`;
|
||||
const r2 = await run('bash', [join(ROOT, 'packaging', 'ota', 'publish-manifest.sh'), ...args2]);
|
||||
if (r2.code !== 0) throw new Error(`publish-manifest.sh (next folder) exited ${r2.code}; retry: ${cmd2.replace(TOKEN_NEXT, '<token.next>')}`);
|
||||
const m2 = JSON.parse(readFileSync(join(DEST_NEXT, 'igneum-app-latest.json'), 'utf8'));
|
||||
const diffs = manifestDifferences(m, m2, BASE, BASE_NEXT);
|
||||
if (diffs.length) throw new Error(`the two manifests differ beyond the folder and the publish time:\n ${diffs.join('\n ')}`);
|
||||
if (tuningFile) say(` tuning carried into the next folder (${Object.keys(m.tuning.cards).length} card model(s))`);
|
||||
done('manifest', 'ok', `${VERSION} ${Object.keys(m.platforms).join('+')} in both folders, signed, verified locally, same fields`);
|
||||
} finally { rmSync(tmp, { recursive: true, force: true }); }
|
||||
}
|
||||
|
||||
async function deploy() {
|
||||
|
|
@ -426,53 +517,62 @@ async function deploy() {
|
|||
done('deploy', 'ok', 'downloads folder deployed');
|
||||
}
|
||||
|
||||
async function verify() {
|
||||
const files = [MAC && DMG_NAME, WIN && SETUP_NAME, WIN && ZIP_NAME].filter(Boolean);
|
||||
if (DRY) return done('verify', 'would', `HEAD and GET ${files.join(', ')} (size and sha256 against the local files), GET the manifest and its .sig, igneum-ota-sign verify`);
|
||||
// one folder: HEAD and GET of the files against the local copies, the manifest's bytes and signature; the failures
|
||||
async function verifyFolder(dest, base, files, label) {
|
||||
const rows = [['file', 'local', 'HEAD', 'sha256']];
|
||||
const failures = [];
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'igneum-ship-'));
|
||||
try {
|
||||
for (const name of files) {
|
||||
const local = join(DEST, name); const want = sha256(local); const size = sizeOf(local);
|
||||
const local = join(dest, name); const want = sha256(local); const size = sizeOf(local);
|
||||
let h, got = '';
|
||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||
h = await head(`${BASE}/${name}`);
|
||||
h = await head(`${base}/${name}`);
|
||||
if (h.status === 200 && (h.length === null || h.length === size)) {
|
||||
const r = await run('curl', ['-fsSL', '--retry', '3', '-o', join(tmp, name), `${BASE}/${name}`], { quiet: true });
|
||||
const r = await run('curl', ['-fsSL', '--retry', '3', '-o', join(tmp, name), `${base}/${name}`], { quiet: true });
|
||||
if (r.code === 0) { got = sha256(join(tmp, name)); if (got === want) break; }
|
||||
}
|
||||
if (attempt < 3) { say(` ${name}: not matching yet (HTTP ${h.status}, length ${h.length}, sha ${got ? got.slice(0, 12) : '-'}); again in 15 s`); await sleep(15000); }
|
||||
}
|
||||
const ok = h.status === 200 && (h.length === null || h.length === size) && got === want;
|
||||
rows.push([name, `${size} B ${want.slice(0, 12)}`, `${h.status} ${h.length === null ? '(no length)' : h.length + ' B'}`, got === want ? `ok ${want.slice(0, 12)}` : `MISMATCH ${got.slice(0, 12) || 'no body'}`]);
|
||||
if (!ok) failures.push(name);
|
||||
if (!ok) failures.push(`${label}:${name}`);
|
||||
state.files = state.files || {}; state.files[name] = { size, sha256: want, served: ok };
|
||||
}
|
||||
// the manifest: bytes and signature, through the same verifier the apps use
|
||||
const mr = await run('curl', ['-fsSL', '-o', join(tmp, 'm.json'), `${BASE}/igneum-app-latest.json`], { quiet: true });
|
||||
const sr = await run('curl', ['-fsSL', '-o', join(tmp, 'm.sig'), `${BASE}/igneum-app-latest.json.sig`], { quiet: true });
|
||||
const mr = await run('curl', ['-fsSL', '-o', join(tmp, 'm.json'), `${base}/igneum-app-latest.json`], { quiet: true });
|
||||
const sr = await run('curl', ['-fsSL', '-o', join(tmp, 'm.sig'), `${base}/igneum-app-latest.json.sig`], { quiet: true });
|
||||
let mline = 'not reachable';
|
||||
if (mr.code === 0 && sr.code === 0) {
|
||||
const v = await run(SIGNER, ['verify', join(CFG, 'ota-signing-key.pub'), join(tmp, 'm.json'), join(tmp, 'm.sig')], { quiet: true });
|
||||
const m = JSON.parse(readFileSync(join(tmp, 'm.json'), 'utf8'));
|
||||
const same = readFileSync(join(tmp, 'm.json')).equals(readFileSync(join(DEST, 'igneum-app-latest.json')));
|
||||
const same = readFileSync(join(tmp, 'm.json')).equals(readFileSync(join(dest, 'igneum-app-latest.json')));
|
||||
const plat = Object.entries(m.platforms || {}).map(([k, e]) => `${k} ${e.sha256.slice(0, 12)}`).join(', ');
|
||||
mline = `${m.version} ${v.code === 0 ? 'signature ok' : 'SIGNATURE FAILS'}${same ? '' : ' (DIFFERS from the local manifest)'} ${plat}`;
|
||||
if (v.code !== 0 || m.version !== VERSION || !same) failures.push('manifest');
|
||||
state.manifest = { version: m.version, published_at: m.published_at, platforms: m.platforms };
|
||||
} else failures.push('manifest');
|
||||
const inFolder = Object.values(m.platforms || {}).every(e => typeof e.url === 'string' && e.url.startsWith(base + '/'));
|
||||
mline = `${m.version} ${v.code === 0 ? 'signature ok' : 'SIGNATURE FAILS'}${same ? '' : ' (DIFFERS from the local manifest)'}${inFolder ? '' : ' (URLS POINT OUTSIDE THIS FOLDER)'} ${plat}`;
|
||||
if (v.code !== 0 || m.version !== VERSION || !same || !inFolder) failures.push(`${label}:manifest`);
|
||||
if (!state.manifest || label === 'current') state.manifest = { version: m.version, published_at: m.published_at, platforms: m.platforms };
|
||||
} else failures.push(`${label}:manifest`);
|
||||
rows.push(['igneum-app-latest.json', '', '', mline]);
|
||||
} finally { rmSync(tmp, { recursive: true, force: true }); }
|
||||
say(` ${label} folder: ${label === 'next' ? 'dl/<token.next>/' : 'dl/<token>/'}`);
|
||||
table(rows);
|
||||
return failures;
|
||||
}
|
||||
|
||||
async function verify() {
|
||||
const files = [MAC && DMG_NAME, WIN && SETUP_NAME, WIN && ZIP_NAME].filter(Boolean);
|
||||
if (DRY) return done('verify', 'would', `HEAD and GET ${files.join(', ')} (size and sha256 against the local files), GET the manifest and its .sig, igneum-ota-sign verify${BOTH ? '; the same for dl/<token.next>/' : ''}`);
|
||||
const failures = await verifyFolder(DEST, BASE, files, 'current');
|
||||
if (BOTH) failures.push(...await verifyFolder(DEST_NEXT, BASE_NEXT, files, 'next'));
|
||||
saveState();
|
||||
if (failures.length) throw new Error(`not served as expected: ${failures.join(', ')}; the deploy may still be propagating. Retry: ${retryCmd('deploy')}`);
|
||||
done('verify', 'ok', `${files.length} files and the manifest match the local copies`);
|
||||
done('verify', 'ok', `${files.length} files and the manifest match the local copies${BOTH ? ' in both folders' : ''}`);
|
||||
}
|
||||
|
||||
async function consoleStep() {
|
||||
const lines = Object.entries(state.files || {}).map(([n, f]) => `${n} ${f.size} B sha256 ${f.sha256}`);
|
||||
const body = [NOTES, ...lines, state.runId ? `Windows CI https://github.com/${REPO}/actions/runs/${state.runId}` : '', state.sha ? `commit ${state.sha.slice(0, 12)}, node fork ${state.forkCommit || '?'}` : ''].filter(Boolean).join('\n');
|
||||
const body = [NOTES, ...lines, state.runId ? `Windows CI https://github.com/${REPO}/actions/runs/${state.runId}` : '', state.sha ? `commit ${state.sha.slice(0, 12)}, node fork ${state.forkCommit || '?'}` : '', BOTH ? 'published in both downloads folders (token rotation)' : ''].filter(Boolean).join('\n');
|
||||
const meta = { version: VERSION, files: state.files || {}, run: state.runId || null, commit: state.sha || null, fork: state.forkCommit || null, manifest_published_at: state.manifest ? state.manifest.published_at : null };
|
||||
if (DRY) return done('console', 'would', `tools/console.mjs post --kind build --key ship:${VERSION} --title "Igneum Miner ${VERSION} shipped" (sizes, hashes, run, commit), then sync-dl`);
|
||||
const r = await run('node', [join(ROOT, 'tools', 'console.mjs'), 'post', '--kind', 'build', '--key', `ship:${VERSION}`, '--title', `Igneum Miner ${VERSION} shipped (${[MAC && 'mac', WIN && 'windows'].filter(Boolean).join('+')})`, '--body', body, '--meta', JSON.stringify(meta)], { quiet: true });
|
||||
|
|
@ -482,7 +582,7 @@ async function consoleStep() {
|
|||
}
|
||||
|
||||
// ---- the runner ----------------------------------------------------------------------------------------------------
|
||||
const IMPL = { preflight, bump, inputs, commit, ci, fetch: fetchStep, dmg, copy, manifest, deploy, verify, console: consoleStep };
|
||||
const IMPL = { preflight, bump, inputs, commit, ci, fetch: fetchStep, dmg, copy, mirror, manifest, deploy, verify, console: consoleStep };
|
||||
async function main() {
|
||||
const start = flags.from ? STEPS.indexOf(flags.from) : 0;
|
||||
// preflight always runs: it is reads only and the later steps need its facts (fork commit, state)
|
||||
|
|
@ -555,6 +655,23 @@ function selfTest() {
|
|||
check('1.2 is refused', refused);
|
||||
check('comma helpers round-trip', toComma('0.3.4') === '0,3,4,0' && fromComma('0,3,4,0') === '0.3.4' && fromComma('0,3,4,1') === '0,3,4,1');
|
||||
check('version compare', cmpVersion('0.3.4', '0.3.3') > 0 && cmpVersion('0.10.0', '0.9.9') > 0 && cmpVersion('1.0.0', '1.0.0') === 0);
|
||||
// 5. --dl-both helpers on two scratch folders
|
||||
const a = join(dir, 'dl', 'old'), b = join(dir, 'dl', 'new');
|
||||
mkdirSync(a, { recursive: true }); mkdirSync(b, { recursive: true });
|
||||
writeFileSync(join(a, 'x.dmg'), 'dmg bytes'); writeFileSync(join(a, 'same.json'), 'same'); writeFileSync(join(b, 'same.json'), 'same');
|
||||
writeFileSync(join(a, 'differs.zip'), 'v2'); writeFileSync(join(b, 'differs.zip'), 'v1');
|
||||
const plan = Object.fromEntries(mirrorPlan(a, b, ['x.dmg', 'same.json', 'differs.zip', 'absent.sig']).map(p => [p.name, p.action]));
|
||||
check('mirror plan: missing -> copy, same -> same, different -> copy, absent -> absent', plan['x.dmg'] === 'copy' && plan['same.json'] === 'same' && plan['differs.zip'] === 'copy' && plan['absent.sig'] === 'absent', JSON.stringify(plan));
|
||||
const first = { version: '0.3.6', published_at: '2026-10-05T12:00:00Z', channel: 'devnet', notes: 'n', min_supported_version: '0.3.0', platforms: { mac: { url: 'https://dl.igneum.network/dl/OLD/Igneum-Miner-0.3.6.dmg', sha256: 'aa', size: 1, kind: 'dmg' } }, consensus: { activation_height: null, deadline_note: '', override: { difficulty_v2_activation_daa: 33000 } }, tuning: { cards: { 'RTX 5090': { v: 1 } } } };
|
||||
const [args, tuningFile] = secondManifestArgs(first, '/dest', 'https://dl.igneum.network/dl/NEW', dir);
|
||||
check('second manifest args carry override, min_supported and tuning', args.includes('--override') && args[args.indexOf('--override') + 1] === '{"difficulty_v2_activation_daa":33000}' && args.includes('--min-supported') && args[args.indexOf('--min-supported') + 1] === '0.3.0' && args.includes('--tuning') && tuningFile && JSON.parse(readFileSync(tuningFile, 'utf8')).cards['RTX 5090'].v === 1, args.join(' '));
|
||||
const [args0] = secondManifestArgs({ version: '0.3.6', platforms: {} }, '/dest', 'https://x', dir);
|
||||
check('second manifest args without override or tuning say --no-tuning and no --override', args0.includes('--no-tuning') && !args0.includes('--override') && !args0.includes('--min-supported'), args0.join(' '));
|
||||
const second = JSON.parse(JSON.stringify(first)); second.published_at = '2026-10-05T12:01:00Z'; second.platforms.mac.url = 'https://dl.igneum.network/dl/NEW/Igneum-Miner-0.3.6.dmg';
|
||||
check('two manifests that differ only by folder and time agree', manifestDifferences(first, second, 'https://dl.igneum.network/dl/OLD', 'https://dl.igneum.network/dl/NEW').length === 0);
|
||||
second.consensus.override.difficulty_v2_activation_daa = 1; delete second.tuning;
|
||||
const d = manifestDifferences(first, second, 'https://dl.igneum.network/dl/OLD', 'https://dl.igneum.network/dl/NEW');
|
||||
check('a changed override and a dropped tuning are reported', d.length === 2 && d.some(x => x.startsWith('consensus')) && d.some(x => x.startsWith('tuning')), d.join(' | '));
|
||||
} finally { rmSync(dir, { recursive: true, force: true }); }
|
||||
say(fails ? `${fails} check(s) failed` : 'all checks passed');
|
||||
return fails ? 1 : 0;
|
||||
|
|
|
|||
Loading…
Reference in a new issue