Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .github/workflows/publish-opensuse.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: Publish openSUSE Repository

on:
workflow_call:
inputs:
tag:
type: string
required: true
operation:
type: string
default: publish
workflow_dispatch:
inputs:
operation:
description: Publish a stable release, refresh expiry, or roll back to a retained revision
type: choice
options: [publish, refresh, rollback]
default: publish
tag:
description: Existing stable release tag for publish
type: string
revision:
description: Retained repository revision SHA-256 for rollback
type: string
schedule:
- cron: "17 6 * * 1"

permissions:
contents: read

concurrency:
group: opensuse-repository-production
cancel-in-progress: false

jobs:
publish:
if: >-
${{
vars.OPENSUSE_REPOSITORY_ENABLED == 'true' &&
(github.ref == format('refs/heads/{0}', github.event.repository.default_branch) ||
(github.workflow == 'Release' && startsWith(github.ref, 'refs/tags/v')))
}}
runs-on: ubuntu-24.04
timeout-minutes: 40
environment: packages-production
container:
image: opensuse/tumbleweed@sha256:b6821dbfad5422b663e0eeae8241a30ef2976e1e9ae4a2f827641c0eecf7e4fd
steps:
- name: Install repository and workflow tools
run: >-
zypper --non-interactive --gpg-auto-import-keys refresh &&
zypper --non-interactive install
createrepo_c gh git gpg2 nodejs24 openssh openssl python3 rpm-build

- name: Checkout publisher
uses: actions/checkout@v7.0.0
with:
fetch-depth: 0
persist-credentials: false

- name: Build, publish, and verify repository
env:
GH_TOKEN: ${{ github.token }}
OPERATION: ${{ inputs.operation || 'refresh' }}
RELEASE_TAG: ${{ inputs.tag }}
ROLLBACK_REVISION: ${{ inputs.revision }}
OPENSUSE_REPOSITORY_URL: ${{ vars.OPENSUSE_REPOSITORY_URL }}
OPENSUSE_REPOSITORY_HOST: ${{ vars.OPENSUSE_REPOSITORY_HOST }}
OPENSUSE_REPOSITORY_ROOT: ${{ vars.OPENSUSE_REPOSITORY_ROOT }}
OPENSUSE_SSH_PORT: ${{ vars.OPENSUSE_SSH_PORT || '22' }}
OPENSUSE_SIGNING_FINGERPRINT: ${{ vars.OPENSUSE_SIGNING_FINGERPRINT }}
OPENSUSE_SSH_PRIVATE_KEY: ${{ secrets.OPENSUSE_SSH_PRIVATE_KEY }}
OPENSUSE_SSH_KNOWN_HOSTS: ${{ secrets.OPENSUSE_SSH_KNOWN_HOSTS }}
OPENSUSE_SIGNING_KEY: ${{ secrets.OPENSUSE_SIGNING_KEY }}
OPENSUSE_SIGNING_PASSPHRASE: ${{ secrets.OPENSUSE_SIGNING_PASSPHRASE }}
run: bash scripts/publish-opensuse-workflow.sh

- name: Upload public verification and activation record
uses: actions/upload-artifact@v7.0.1
with:
name: loopwire-opensuse-publication-${{ github.run_id }}
path: dist/opensuse-publication
if-no-files-found: error
retention-days: 90
10 changes: 10 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -523,3 +523,13 @@ jobs:
tag: ${{ needs.publish-release.outputs.tag }}
operation: publish
secrets: inherit

publish-opensuse:
name: Publish signed openSUSE channel
needs: publish-release
if: ${{ vars.OPENSUSE_REPOSITORY_ENABLED == 'true' && !contains(needs.publish-release.outputs.tag, '-') }}
uses: ./.github/workflows/publish-opensuse.yml
with:
tag: ${{ needs.publish-release.outputs.tag }}
operation: publish
secrets: inherit
2 changes: 2 additions & 0 deletions .github/workflows/workflow-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
- "scripts/test-ci-workflow-paths.rb"
- "scripts/test-apt-workflow.rb"
- "scripts/test-fedora-workflow.rb"
- "scripts/test-opensuse-workflow.rb"
- "scripts/*native-package-proof-snapshot.mjs"
- "scripts/verify-github-workflows.sh"
- "scripts/verify-requirements.sh"
Expand All @@ -25,6 +26,7 @@ on:
- "scripts/test-ci-workflow-paths.rb"
- "scripts/test-apt-workflow.rb"
- "scripts/test-fedora-workflow.rb"
- "scripts/test-opensuse-workflow.rb"
- "scripts/*native-package-proof-snapshot.mjs"
- "scripts/verify-github-workflows.sh"
- "scripts/verify-requirements.sh"
Expand Down
58 changes: 58 additions & 0 deletions .planning/quick/260905-nrs-signed-opensuse/260905-nrs-PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
status: implementing
issue: 37
depends_on: 46
---

# Signed openSUSE repository development

Goal: complete every development task in #37 and open a dedicated PR containing `resolves #37`. This branch is an
intentional stack on #36/PR #46 because the openSUSE channel reuses the exact-release provenance, RPM signing,
content-addressed metadata retention, protected publication, and KVM proof foundations introduced there. Production
provider ownership, credentials, signing identity, GitHub environment configuration, and first public activation
remain the separately listed human operational tasks.

## Decisions

- Choose a project-owned repository over OBS. It publishes the exact authenticated project release RPM, preserves the
release/source/build identity already recorded by Loopwire, and gives the project explicit promotion, retention,
rollback, and recovery semantics. OBS would rebuild and sign a distinct output, requiring provider project/build
identities and provider-specific proof that cannot be produced without the human operational setup.
- Initial scope is openSUSE Tumbleweed x86_64 only. Leap and other architectures remain unsupported until they receive
their own package and clean-guest validation.
- Reuse the shared RPM repository protocol only where libzypp/Zypper behavior proves it compatible. Keep the target,
client bootstrap, workflow, public activation record, docs, and guest proof openSUSE-specific.
- Require authenticated repository metadata and embedded RPM signatures. The bootstrap path must preserve Zypper
checks and may not use `--no-gpg-checks` or `--allow-unsigned-rpm`.
- Retain immutable signed RPMs and content-addressed metadata indefinitely in v1. Publication must serialize writers,
require revision CAS, reject immutable collisions, recover interruption, and expose only complete revisions or a
verification failure during promotion.
- The checked-in openSUSE channel remains pending. Existing signed direct-download and automatic-installer paths stay
visible until a maintainer completes and reviews the production public proof record.

## Work lanes

1. Repository protocol: extend exact-release authentication, RPM/repository signing, metadata manifests, rollback, and
tamper tests for the openSUSE Tumbleweed target, grounded in real Zypper/libzypp behavior.
2. Publication: extend local/SSH publication, atomic promotion, CAS/locks/recovery, immutable retention, and public
HTTP proof for the openSUSE namespace.
3. Guest proof: run an isolated clean Tumbleweed KVM lifecycle using the actual public v0.1.0 openSUSE RPM, plus a
strictly synthetic upgrade, and verify raw provenance, installed bytes, providers, backend JSON, and GUI linkage.
4. Product/docs: add the pending/verified homepage gate, tested bootstrap, provider decision, rolling-snapshot policy,
support matrix, release/operator guidance, navigation, and unreleased notes.
5. Root integration: protected workflow, config contracts, focused and full gates, browser fixtures, final review,
issue checklist update, and PR delivery.

## Required verification

- Real Zypper accepts correct signed metadata and package content and rejects wrong, unsigned, or tampered content.
Version, architecture, target, vendor/origin, downgrade, and repository removal behavior are independently verified.
- Publication proves writer exclusion, CAS, ordering, idempotence, interruption recovery, permissions, immutable
retention, rollback, actual SSH transport, and public cache behavior without production credentials.
- A clean checksum-pinned Tumbleweed KVM guest performs repository install, reinstall, fixture upgrade, explicit
rollback/downgrade, removal, and repository removal against final committed development code. Evidence records the
snapshot and binds source/distributed hashes, signer, origin/vendor, versions, installed bytes, providers, and GUI.
- Documentation defines a repeatable later-snapshot compatibility run and requires disabling activation/escalating a
failed snapshot until the package or compatibility declaration is repaired and the full lifecycle reruns.
- Pending and verified-fixture browser states, workflow/action syntax, docs, focused tests, and full project gates pass.
Public production remains disabled and no human task is reported complete without its real evidence.
88 changes: 88 additions & 0 deletions .planning/quick/260905-nrs-signed-opensuse/260905-nrs-SUMMARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
status: complete
issue: 37
depends_on: 46
---

# Signed openSUSE repository development

Issue: https://github.com/sandwichfarm/loopwire/issues/37

## Result and stack

The openSUSE Tumbleweed x86_64 development work is complete. This branch intentionally stacks on #36/PR #46 so the
channel can reuse the reviewed exact-release provenance, RPM signing, SSH/POSIX publication, protected environment,
and KVM foundations while retaining an independent target, namespace, state, workflow, bootstrap, activation record,
documentation, and guest proof. The checked-in channel remains `pending`; the five Human operational tasks still own
production hosting, signing custody, GitHub configuration, first public publication, and activation.

## Development checklist evidence

1. **Provider evaluation:** the maintainer runbook compares OBS and project-owned delivery across output identity,
signing, promotion/recovery, and rolling compatibility. Project-owned delivery was selected because it consumes the
exact authenticated GitHub Release RPM and supports the existing independently testable publication protocol. OBS
would produce a provider build requiring provisioned project/build identities and separate provider proof.
2. **Package path:** the generator accepts only the Tumbleweed x86_64 `loopwire-VERSION-1.x86_64.rpm`, authenticates
the OpenSSL-signed SHA256SUMS plus release-assets manifest, and binds the exact RPM and x86_64 archive to the stable
tag and public release commit. The public source RPM is never mutated; only a staged copy is repository-signed.
3. **Signing:** an isolated OpenPGP identity signs the staged RPM and `repodata/repomd.xml`. The manifest and signed
metadata retain separate source/distributed hashes and the public source revision. Zypper is configured with
`gpgcheck=1`, `repo_gpgcheck=1`, and `pkg_gpgcheck=1`; wrong, unsigned, and tampered inputs fail closed.
4. **Publication/rollback:** openSUSE uses `/opensuse/tumbleweed/x86_64` publicly and isolated private locks, CAS,
journals, and snapshots under `channels/opensuse-tumbleweed-x86_64`. Immutable objects are retained indefinitely in
v1. Signature-first/atomic-metadata promotion, interruption recovery, retry idempotence, and freshly signed rollback
were tested with real Zypper, actual SSH, and live Nginx cache behavior.
5. **Protected automation:** Publish openSUSE Repository uses a checksum-pinned Tumbleweed toolchain and supports stable
release publication, weekly refresh, and retained-revision rollback. `OPENSUSE_REPOSITORY_ENABLED=true` plus the
`packages-production` environment gate writes. The job waits for existing release gates, verifies public release
inputs, publishes over pinned SSH, verifies served HTTPS bytes, and emits a reviewable activation record.
6. **Bootstrap:** the repeat-safe helper accepts Tumbleweed x86_64 only, downloads the fingerprint-addressed public key
over HTTPS, verifies one complete primary fingerprint before writes, and atomically installs only its managed key
and `.repo` file. Dry-run has no network or writes. Removal preserves packages, accepted RPM-database trust, and
unrelated repositories. Docs cover the interactive key prompt, rotation, vendor protection, priority, and cleanup.
7. **Regression tests:** real Zypper accepts the signed repository and rejects wrong/malformed keys, missing/changed
signatures, changed metadata, and unsigned/tampered/wrong-key RPMs. Generator and publisher tests cover target,
NEVRA/version/architecture, release provenance, downgrade/repack rejection, deterministic retention, encrypted
keys, unsafe paths, locks, CAS, every interruption point, permissions, SSH transport, HTTP caching, and rollback.
8. **Tumbleweed lifecycle/compatibility:** a checksum-pinned clean Tumbleweed `20260829` KVM guest used the actual
public v0.1.0 openSUSE RPM and authenticated release manifest. It installed and reinstalled the baseline, upgraded
only to the declared synthetic `+zypperfixture1`, rolled back to the public baseline, removed the package, isolated
trust database, and repository. Evidence binds source/distributed hashes, release commit, signature, transaction
origin, active candidate, native installed view, vendor, `/usr` bytes, providers, backend JSON, GUI linkage, and a
real X11 window at every installed stage. Later snapshot runs consume the same explicit target manifest during run
and verification; a failure blocks activation until repaired or compatibility is narrowed and the lifecycle reruns.
9. **Docs/UI:** homepage, install guide, support matrix, release guide, user/operator guides, packaging docs, navigation,
and unreleased notes are updated. Pending preserves the authenticated direct-RPM/automatic installer fallback;
verified-fixture proof switches only openSUSE to `sudo zypper install loopwire` with separate setup guidance. The
repository is identified as third-party and never described as default-distribution or publicly active.

## Verification

- Final pre-guest `pnpm check` passed requirements/docs, automation, workflow contracts, runtime/install/package gates,
types, 295 workspace tests, 22 Rust tests, production builds, static-site checks, and APT/Fedora/openSUSE suites.
- `pnpm verify:opensuse-repository` passed in the pinned Tumbleweed image: 7 generator, 27 publisher, 7 bootstrap,
6 public HTTPS, 3 workflow preflight, workflow contract, 39 raw proof-verifier, and 5 channel-gate cases. The publisher
exercised actual SSH and real Zypper failure/recovery around mixed metadata.
- Fedora regression verification passed: 13 generator and 27 publisher cases plus bootstrap, public, workflow, proof,
and UI gates, including real DNF behavior.
- The clean KVM lifecycle at `08a18e4484627e36233ab068891bc468e9613f84` produced 159 evidence files and passed an
independent replay. The guest is snapshot `20260829`; its image SHA-256 is
`e80d2d1f9cfb328c79a5031d6a30f9744ec83824f6ba44c41ce831ff829a5dad`. Public baseline source SHA-256 is
`f6bc10589e6308fdc405faa104835cd6bcc486c4bc3fba95b5808b94267f1d05`, bound to public release commit
`dfdecf30d681c553a906ceebb13c859bb1b46ef3`.
- Pending and verified-fixture Chromium suites passed all platform, command, keyboard/copy, no-JavaScript, responsive,
motion/reduced-motion/visibility, openSUSE guide, and setup-link assertions. Correct selected-tab screenshots passed
visual verdict at 96/100; the checked-in channel and final build were restored to pending.
- Actionlint, ShellCheck, Python/Node/Ruby/Bash syntax, docs/workflow contracts, and whitespace passed. Comprehensive
review findings for read-only syntax, snapshot-manifest propagation, and workflow path coverage were fixed and the
re-review approved the result.

## Boundaries

The issue's five Human operational tasks remain unchecked. No production account, OBS project, host, TLS/DNS,
OpenPGP identity, SSH credential, GitHub environment value, repository publication, or website activation was created
or changed. Fixture keys, CA, SSH server, synthetic upgrade, and isolated RPM trust database are disposable.

Zypper authenticates repository metadata and RPMs but does not enforce Loopwire's custom signed verification deadline;
HTTPS/origin control, weekly refresh, and monitoring remain operational requirements. Power-loss and network
filesystems were not exercised; production requires local POSIX lock/fsync/atomic-rename behavior.
2 changes: 2 additions & 0 deletions apps/docs/docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export default defineConfig({
{ text: "Install", link: "/guide/install" },
{ text: "APT Repository", link: "/guide/apt-repository" },
{ text: "Fedora Repository", link: "/guide/fedora-repository" },
{ text: "openSUSE Repository", link: "/guide/opensuse-repository" },
{ text: "Basic Usage", link: "/guide/basic-usage" },
{ text: "Configurations", link: "/guide/configurations" },
{ text: "Audio Backends", link: "/guide/backends" },
Expand All @@ -64,6 +65,7 @@ export default defineConfig({
{ text: "Release", link: "/developer/release" },
{ text: "APT Repository Operations", link: "/developer/apt-repository" },
{ text: "Fedora Repository Operations", link: "/developer/fedora-repository" },
{ text: "openSUSE Repository Operations", link: "/developer/opensuse-repository" },
{ text: "Release Notes", link: "/developer/release-notes" }
]
},
Expand Down
Loading