Skip to content

Commit 4e8e58c

Browse files
committed
fix: resolve status command to ~/.auths, add release signing and workflow scripts
1 parent e8687c5 commit 4e8e58c

3 files changed

Lines changed: 319 additions & 14 deletions

File tree

.github/workflows/release.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,49 @@ jobs:
9898
$hash = (Get-FileHash ${{ matrix.asset_name }}${{ matrix.ext }} -Algorithm SHA256).Hash.ToLower()
9999
"$hash ${{ matrix.asset_name }}${{ matrix.ext }}" | Out-File -Encoding ascii ${{ matrix.asset_name }}${{ matrix.ext }}.sha256
100100
101+
- name: Install auths for artifact signing (Unix)
102+
if: matrix.ext == '.tar.gz'
103+
run: |
104+
cargo build --release --package auths-cli
105+
sudo cp target/release/auths /usr/local/bin/auths
106+
107+
- name: Sign artifact (Unix)
108+
if: matrix.ext == '.tar.gz'
109+
env:
110+
AUTHS_PASSPHRASE: ${{ secrets.AUTHS_CI_PASSPHRASE }}
111+
AUTHS_CI_KEYCHAIN_B64: ${{ secrets.AUTHS_CI_KEYCHAIN }}
112+
AUTHS_CI_IDENTITY_BUNDLE_B64: ${{ secrets.AUTHS_CI_IDENTITY_BUNDLE }}
113+
AUTHS_KEYCHAIN_BACKEND: file
114+
AUTHS_KEYCHAIN_FILE: /tmp/auths-ci-keychain
115+
run: |
116+
if [ -z "$AUTHS_PASSPHRASE" ] || [ -z "$AUTHS_CI_KEYCHAIN_B64" ] || [ -z "$AUTHS_CI_IDENTITY_BUNDLE_B64" ]; then
117+
echo "Skipping artifact signing: AUTHS_CI_PASSPHRASE, AUTHS_CI_KEYCHAIN, and AUTHS_CI_IDENTITY_BUNDLE must all be set (run 'just ci-setup' to populate them)"
118+
exit 0
119+
fi
120+
121+
printf '%s' "$AUTHS_CI_KEYCHAIN_B64" | tr -d '[:space:]' | base64 -d > /tmp/auths-ci-keychain
122+
mkdir -p /tmp/auths-identity
123+
printf '%s' "$AUTHS_CI_IDENTITY_BUNDLE_B64" | tr -d '[:space:]' | base64 -d | tar -xz -C /tmp/auths-identity
124+
125+
if ! git -C /tmp/auths-identity rev-parse --git-dir > /dev/null 2>&1; then
126+
echo "Skipping artifact signing: AUTHS_CI_IDENTITY_BUNDLE does not contain a valid git repository."
127+
echo "Re-run 'just ci-setup' to regenerate the secret, then update AUTHS_CI_IDENTITY_BUNDLE in GitHub Secrets."
128+
exit 0
129+
fi
130+
131+
auths artifact sign ${{ matrix.asset_name }}${{ matrix.ext }} \
132+
--device-key-alias ci-release-device \
133+
--note "GitHub Actions release — ${{ github.ref_name }}" \
134+
--repo /tmp/auths-identity
135+
101136
- name: Upload artifact
102137
uses: actions/upload-artifact@v4
103138
with:
104139
name: ${{ matrix.asset_name }}
105140
path: |
106141
${{ matrix.asset_name }}${{ matrix.ext }}
107142
${{ matrix.asset_name }}${{ matrix.ext }}.sha256
143+
${{ matrix.asset_name }}${{ matrix.ext }}.auths.json
108144
109145
release:
110146
needs: build

crates/auths-cli/src/commands/status.rs

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::ux::format::{JsonResponse, Output, is_json_mode};
44
use anyhow::{Result, anyhow};
55
use auths_id::storage::attestation::AttestationSource;
66
use auths_id::storage::identity::IdentityStorage;
7+
use auths_id::storage::layout;
78
use auths_storage::git::{RegistryAttestationStorage, RegistryIdentityStorage};
89
use chrono::{DateTime, Duration, Utc};
910
use clap::Parser;
@@ -358,21 +359,9 @@ fn get_auths_dir() -> Result<PathBuf> {
358359
auths_core::paths::auths_home().map_err(|e| anyhow!(e))
359360
}
360361

361-
/// Resolve the repository path from optional argument or default.
362+
/// Resolve the repository path from optional argument or default (~/.auths).
362363
fn resolve_repo_path(repo_arg: Option<PathBuf>) -> Result<PathBuf> {
363-
match repo_arg {
364-
Some(pathbuf) if !pathbuf.as_os_str().is_empty() => Ok(pathbuf),
365-
_ => {
366-
// Try current directory first
367-
let cwd = std::env::current_dir()?;
368-
if crate::factories::storage::discover_git_repo(&cwd).is_ok() {
369-
Ok(cwd)
370-
} else {
371-
// Fall back to ~/.auths
372-
get_auths_dir()
373-
}
374-
}
375-
}
364+
layout::resolve_repo_path(repo_arg).map_err(|e| anyhow!(e))
376365
}
377366

378367
/// Check if a process with the given PID is running.
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Test the full release artifact signing workflow locally (macOS aarch64).
4+
5+
Usage:
6+
python scripts/auths_workflows/artifact_signing.py # run the full workflow
7+
python scripts/auths_workflows/artifact_signing.py --skip-build # reuse existing build
8+
9+
What it does:
10+
1. Checks prerequisites (cargo, auths identity, device keys)
11+
2. Builds release binaries (cargo build --release -p auths-cli)
12+
3. Packages them into auths-macos-aarch64.tar.gz (same as CI)
13+
4. Generates SHA256 checksum
14+
5. Signs the artifact with `auths artifact sign`
15+
6. Displays the .auths.json attestation
16+
7. Verifies the attestation with `auths artifact verify`
17+
8. Cleans up
18+
19+
Requires:
20+
- macOS aarch64
21+
- cargo on PATH
22+
- auths identity set up (`auths status` shows identity)
23+
- At least one device key alias (`auths key list`)
24+
25+
This mirrors the release.yml workflow so you can validate signing
26+
works before pushing a tag.
27+
"""
28+
29+
import json
30+
import os
31+
import shutil
32+
import subprocess
33+
import sys
34+
import tempfile
35+
from pathlib import Path
36+
37+
REPO_ROOT = Path(__file__).resolve().parents[2]
38+
TARGET = "aarch64-apple-darwin"
39+
ASSET_NAME = "auths-macos-aarch64"
40+
EXT = ".tar.gz"
41+
BINARIES = ["auths", "auths-sign", "auths-verify"]
42+
43+
# ANSI colors
44+
GREEN = "\033[92m"
45+
YELLOW = "\033[93m"
46+
RED = "\033[91m"
47+
CYAN = "\033[96m"
48+
BOLD = "\033[1m"
49+
RESET = "\033[0m"
50+
51+
52+
def step(n: int, msg: str) -> None:
53+
print(f"\n{BOLD}{CYAN}[Step {n}]{RESET} {BOLD}{msg}{RESET}")
54+
55+
56+
def ok(msg: str) -> None:
57+
print(f" {GREEN}{RESET} {msg}")
58+
59+
60+
def warn(msg: str) -> None:
61+
print(f" {YELLOW}{RESET} {msg}")
62+
63+
64+
def fail(msg: str) -> None:
65+
print(f" {RED}{RESET} {msg}", file=sys.stderr)
66+
67+
68+
def run(cmd: list[str], **kwargs) -> subprocess.CompletedProcess:
69+
"""Run a command, print it, and return the result."""
70+
display = " ".join(cmd)
71+
print(f" $ {display}", flush=True)
72+
return subprocess.run(cmd, **kwargs)
73+
74+
75+
def run_checked(cmd: list[str], **kwargs) -> subprocess.CompletedProcess:
76+
result = run(cmd, capture_output=True, text=True, **kwargs)
77+
if result.returncode != 0:
78+
fail(f"Command failed (exit {result.returncode})")
79+
if result.stderr.strip():
80+
print(f" stderr: {result.stderr.strip()}")
81+
sys.exit(1)
82+
return result
83+
84+
85+
def main() -> None:
86+
skip_build = "--skip-build" in sys.argv
87+
88+
print(f"{BOLD}{'='*60}{RESET}")
89+
print(f"{BOLD} Artifact Signing Workflow — Local Test{RESET}")
90+
print(f"{BOLD}{'='*60}{RESET}")
91+
92+
# ── Step 1: Prerequisites ──
93+
step(1, "Checking prerequisites")
94+
95+
if shutil.which("cargo") is None:
96+
fail("cargo not found on PATH")
97+
sys.exit(1)
98+
ok("cargo found")
99+
100+
if shutil.which("auths") is None:
101+
fail("auths not found on PATH. Run: cargo install --path crates/auths-cli")
102+
sys.exit(1)
103+
ok("auths found")
104+
105+
# Check identity exists
106+
result = run_checked(["auths", "status"], cwd=REPO_ROOT)
107+
if "not initialized" in result.stdout.lower():
108+
fail("No auths identity found. Run: auths init")
109+
sys.exit(1)
110+
ok("auths identity exists")
111+
112+
print(f"\n {CYAN}--- auths status ---{RESET}")
113+
for line in result.stdout.strip().splitlines():
114+
print(f" {line}")
115+
116+
# Check device keys
117+
key_result = run_checked(["auths", "key", "list"], cwd=REPO_ROOT)
118+
if not key_result.stdout.strip():
119+
fail("No device keys found. You need at least one key alias.")
120+
sys.exit(1)
121+
ok("device keys found")
122+
123+
print(f"\n {CYAN}--- auths key list ---{RESET}")
124+
for line in key_result.stdout.strip().splitlines():
125+
print(f" {line}")
126+
127+
# Ask user which device key alias to use
128+
print()
129+
device_alias = input(f" {BOLD}Enter device-key-alias to use for signing:{RESET} ").strip()
130+
if not device_alias:
131+
fail("No alias provided.")
132+
sys.exit(1)
133+
134+
# Optionally ask for identity key alias
135+
identity_alias = input(
136+
f" {BOLD}Enter identity-key-alias (leave blank for device-only):{RESET} "
137+
).strip()
138+
139+
# ── Step 2: Build ──
140+
work_dir = Path(tempfile.mkdtemp(prefix="auths-release-test-"))
141+
print(f"\n Working directory: {work_dir}")
142+
143+
if skip_build:
144+
step(2, "Skipping build (--skip-build)")
145+
# Verify binaries exist
146+
for binary in BINARIES:
147+
path = REPO_ROOT / "target" / "release" / binary
148+
if not path.exists():
149+
fail(f"Binary not found: {path}")
150+
fail("Run without --skip-build first.")
151+
shutil.rmtree(work_dir)
152+
sys.exit(1)
153+
ok("Existing release binaries found")
154+
else:
155+
step(2, "Building release binaries")
156+
result = run(
157+
["cargo", "build", "--release", "--package", "auths-cli"],
158+
cwd=REPO_ROOT,
159+
)
160+
if result.returncode != 0:
161+
fail("Build failed")
162+
shutil.rmtree(work_dir)
163+
sys.exit(1)
164+
ok("Build complete")
165+
166+
# ── Step 3: Package ──
167+
step(3, "Packaging binaries into tarball")
168+
staging = work_dir / "staging"
169+
staging.mkdir()
170+
171+
for binary in BINARIES:
172+
src = REPO_ROOT / "target" / "release" / binary
173+
if src.exists():
174+
shutil.copy2(src, staging / binary)
175+
ok(f"Copied {binary}")
176+
else:
177+
warn(f"Binary not found: {binary} (skipped)")
178+
179+
tarball = work_dir / f"{ASSET_NAME}{EXT}"
180+
run_checked(
181+
["tar", "-czf", str(tarball), "-C", str(staging), "."],
182+
)
183+
size_mb = tarball.stat().st_size / (1024 * 1024)
184+
ok(f"Created {tarball.name} ({size_mb:.1f} MB)")
185+
186+
# ── Step 4: SHA256 checksum ──
187+
step(4, "Generating SHA256 checksum")
188+
checksum_file = work_dir / f"{ASSET_NAME}{EXT}.sha256"
189+
result = run_checked(["shasum", "-a", "256", str(tarball)])
190+
checksum_line = result.stdout.strip()
191+
checksum_file.write_text(checksum_line + "\n")
192+
ok(f"Checksum: {checksum_line.split()[0]}")
193+
194+
# ── Step 5: Sign artifact ──
195+
step(5, "Signing artifact with auths")
196+
sign_cmd = [
197+
"auths", "artifact", "sign", str(tarball),
198+
"--device-key-alias", device_alias,
199+
"--note", "Local signing test",
200+
]
201+
if identity_alias:
202+
sign_cmd.extend(["--identity-key-alias", identity_alias])
203+
204+
result = run(sign_cmd, cwd=REPO_ROOT)
205+
if result.returncode != 0:
206+
fail("Artifact signing failed")
207+
print(f"\n Working directory preserved at: {work_dir}")
208+
sys.exit(1)
209+
210+
attestation_file = Path(f"{tarball}.auths.json")
211+
if not attestation_file.exists():
212+
fail(f"Expected attestation file not found: {attestation_file}")
213+
print(f"\n Working directory preserved at: {work_dir}")
214+
sys.exit(1)
215+
ok(f"Created {attestation_file.name}")
216+
217+
# ── Step 6: Display attestation ──
218+
step(6, "Attestation contents")
219+
attestation_raw = attestation_file.read_text()
220+
try:
221+
attestation = json.loads(attestation_raw)
222+
formatted = json.dumps(attestation, indent=2)
223+
print(f"\n {CYAN}--- {attestation_file.name} ---{RESET}")
224+
for line in formatted.splitlines():
225+
print(f" {line}")
226+
except json.JSONDecodeError:
227+
print(f" (raw): {attestation_raw[:500]}")
228+
229+
# ── Step 7: Verify attestation ──
230+
step(7, "Verifying attestation")
231+
result = run(
232+
["auths", "artifact", "verify", str(tarball)],
233+
cwd=REPO_ROOT,
234+
capture_output=True,
235+
text=True,
236+
)
237+
if result.returncode == 0:
238+
ok("Verification passed")
239+
if result.stdout.strip():
240+
for line in result.stdout.strip().splitlines():
241+
print(f" {line}")
242+
else:
243+
warn(f"Verification returned exit {result.returncode}")
244+
if result.stdout.strip():
245+
for line in result.stdout.strip().splitlines():
246+
print(f" {line}")
247+
if result.stderr.strip():
248+
for line in result.stderr.strip().splitlines():
249+
print(f" {line}")
250+
251+
# ── Step 8: Summary ──
252+
step(8, "Cleanup and summary")
253+
print(f"\n {CYAN}Files produced:{RESET}")
254+
for f in sorted(work_dir.iterdir()):
255+
if f.is_file():
256+
size = f.stat().st_size
257+
print(f" {f.name:50s} {size:>10,} bytes")
258+
for f in [attestation_file]:
259+
if f.exists() and f.parent != work_dir:
260+
size = f.stat().st_size
261+
print(f" {f.name:50s} {size:>10,} bytes")
262+
263+
# Clean up
264+
shutil.rmtree(work_dir)
265+
if attestation_file.exists():
266+
attestation_file.unlink()
267+
ok("Cleaned up temp files")
268+
269+
print(f"\n{BOLD}{GREEN}{'='*60}{RESET}")
270+
print(f"{BOLD}{GREEN} Artifact signing workflow completed successfully!{RESET}")
271+
print(f"{BOLD}{GREEN}{'='*60}{RESET}")
272+
print(f"\n This confirms the release.yml signing step will work in CI.")
273+
print(f" Make sure these GitHub secrets are set (via 'just ci-setup'):")
274+
print(f" • AUTHS_CI_PASSPHRASE")
275+
print(f" • AUTHS_CI_KEYCHAIN")
276+
print(f" • AUTHS_CI_IDENTITY_BUNDLE\n")
277+
278+
279+
if __name__ == "__main__":
280+
main()

0 commit comments

Comments
 (0)