Skip to content
Open
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
71 changes: 55 additions & 16 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,72 @@ name: Release
on:
push:
tags:
# Match semantic version tags (e.g. v1.2.3, v10.11.12, v12.3.7-beta7)
- 'v*.*.*'

jobs:
goreleaser:
build-packages:
name: Build ${{ matrix.bin }} for ${{ matrix.target }}
runs-on: ubuntu-latest
strategy:
matrix:
include:
- bin: comenq
target: x86_64-unknown-linux-gnu
arch: amd64
- bin: comenqd
target: x86_64-unknown-linux-gnu
arch: amd64
- bin: comenq
target: aarch64-unknown-linux-gnu
arch: arm64
- bin: comenqd
target: aarch64-unknown-linux-gnu
arch: arm64
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
- name: Prepare release version
run: |
set -euo pipefail
version="${GITHUB_REF_NAME#v}"
echo "RELEASE_VERSION=${version}" >> "$GITHUB_ENV"
- name: Clean dist directory
run: rm -rf dist
- name: Build and package ${{ matrix.bin }}
uses: leynos/shared-actions/.github/actions/rust-build-release@7bc9b6c15964ef98733aa647b76d402146284ba3
with:
toolchain: stable
targets: x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu
cache: cargo
- name: Set up Go
uses: actions/setup-go@v6
target: ${{ matrix.target }}
bin-name: ${{ matrix.bin }}
version: ${{ env.RELEASE_VERSION }}
formats: deb,rpm
- name: Upload artefacts
uses: actions/upload-artifact@v4
with:
go-version: '1.21'
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
name: ${{ matrix.bin }}-${{ matrix.arch }}
path: |
dist/**/*.deb
dist/**/*.rpm
dist/nfpm.yaml
dist/.man/**
if-no-files-found: error
release:
name: Publish GitHub release
runs-on: ubuntu-latest
needs: build-packages
steps:
- name: Download artefacts
uses: actions/download-artifact@v4
with:
path: release-artifacts
- name: Create draft release
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836
with:
distribution: goreleaser
# Use a fixed version to ensure reproducibility
version: v1.24.0
args: release --clean
tag_name: ${{ github.ref_name }}
draft: true
files: |
release-artifacts/**/*.deb
release-artifacts/**/*.rpm
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
18 changes: 18 additions & 0 deletions crates/comenq/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//! Stages the packaged client man page for installation.
use std::{env, fs, path::PathBuf};

fn main() {
if let Err(error) = copy_man_page() {
panic!("failed to stage man page: {error}");
}
}

fn copy_man_page() -> std::io::Result<()> {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
let source = manifest_dir.join("../../packaging/man/comenq.1");
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR"));
let dest = out_dir.join("comenq.1");
fs::copy(&source, &dest)?;
println!("cargo:rerun-if-changed={}", source.display());
Ok(())
}
18 changes: 18 additions & 0 deletions crates/comenqd/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//! Stages the packaged daemon man page for installation.
use std::{env, fs, path::PathBuf};

fn main() {
if let Err(error) = copy_man_page() {
panic!("failed to stage man page: {error}");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: Consider using a more descriptive error message for failed man page staging.

Including the source and destination paths in the panic message will make it easier to identify the cause of the failure.

}
}

fn copy_man_page() -> std::io::Result<()> {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
let source = manifest_dir.join("../../packaging/man/comenqd.1");
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR"));
let dest = out_dir.join("comenqd.1");
fs::copy(&source, &dest)?;
println!("cargo:rerun-if-changed={}", source.display());
Ok(())
}
32 changes: 24 additions & 8 deletions docs/automated-cross-platform-packaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,30 @@

This guide provides a step-by-step process for configuring a GitHub Actions
workflow to automatically build and package the `comenq` client and `comenqd`
daemon for Linux (Fedora, Ubuntu) and macOS. macOS packaging is currently on
hold, so the workflow focuses on Linux targets only. GoReleaser manages the
entire process, from building the Rust binaries to creating platform-native
packages (`.rpm`, `.deb`) and a Homebrew formula.

The core of this process involves creating a `.goreleaser.yaml` file that
declaratively defines the build, packaging, and release steps. This file will
be used by a GitHub Actions workflow that triggers on new git tags.
daemon for Linux (Fedora, Ubuntu). macOS packaging remains on hold, so the
workflow focuses on Linux targets only. The current implementation replaces the
earlier GoReleaser flow with composite actions hosted in
`leynos/shared-actions`. These actions orchestrate the Rust build, stage the
man pages generated by the workspace build scripts, and delegate all packaging
to the shared `linux-packages` helper. The helper emits `.rpm` and `.deb`
artefacts for each binary without invoking `nfpm` directly in the workflow.

The workflow matrix iterates over both binaries (`comenq` and `comenqd`) and
the supported Linux targets (`x86_64-unknown-linux-gnu` and
`aarch64-unknown-linux-gnu`). For each combination the `rust-build-release`
action compiles the workspace with the correct target toolchain and stages the
matching man page. The downstream `linux-packages` composite action, executed
by `rust-build-release`, generates the `nfpm` manifest and packages the staged
artefacts in one pass. This keeps the workflow lean—only the shared action ever
executes `nfpm`—and ensures the generated `.deb` and `.rpm` files are ready to
upload. Finally, `softprops/action-gh-release` publishes the generated packages
to a draft GitHub Release, preserving the previous workflow's release review
gate.

The remainder of this document captures the original GoReleaser-oriented plan
for historical context. The high-level packaging requirements remain relevant,
but the automation now lives entirely in `.github/workflows/release.yml` via
the shared composite actions described above.

### Part 1: Packaging for Fedora and Ubuntu with systemd

Expand Down
21 changes: 12 additions & 9 deletions docs/comenq-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -800,15 +800,18 @@ service.

### 4.4. Packaging and Release Workflow

To simplify installation, the project uses GoReleaser. The declarative
`.goreleaser.yaml` builds both binaries via the `goreleaser-rust` plugin,
eliminating manual pre-build hooks. The `nfpms` section produces signed `.deb`
and `.rpm` packages for Fedora and Ubuntu, embedding the hardened `systemd`
service unit and lifecycle scripts that create the `comenq` user. This keeps
packaging logic version controlled and repeatable. A GitHub Actions workflow
triggers on version tags to run GoReleaser. It builds Linux packages and
uploads them to a draft release. Mac support is currently deferred, so the
workflow targets Linux only.
To simplify installation, the project now relies on the composite actions
published in `leynos/shared-actions`. The release workflow iterates over the
`comenq` client and `comenqd` daemon for both the x86_64 and aarch64 GNU/Linux
targets. `rust-build-release` provisions the correct Rust toolchain, compiles
the workspace in release mode, and stages the man pages that each crate's build
script copies from `packaging/man`. Packaging responsibility sits entirely with
the shared `linux-packages` helper invoked by `rust-build-release`; it
generates the transient `nfpm` manifest and emits `.deb` and `.rpm` artefacts
for every matrix entry. The workflow uploads those artefacts to a draft GitHub
Release via `softprops/action-gh-release`, preserving the manual review gate
that existed in the GoReleaser-based flow. macOS support remains deferred, so
the workflow targets Linux only.

## Section 5: Complete Source Code and Project Manifest

Expand Down
37 changes: 37 additions & 0 deletions packaging/man/comenq.1
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.TH COMENQ 1 "May 2024" "comenq" "User Commands"
.SH NAME
comenq \- enqueue a GitHub pull request comment for the comenqd daemon
.SH SYNOPSIS
.B comenq
.I owner/repo
.I pr-number
.I comment
[
.B --socket
.I path
]
.SH DESCRIPTION
The
.B comenq
command enqueues a comment request for the
.BR comenqd (8)
daemon. It takes the target repository in
.I owner/repo
format, the pull request number, and the comment body to submit.
.PP
Use
.B --socket
to override the Unix Domain Socket path used to contact the daemon. The
socket defaults to
.I /run/comenq/comenq.sock
which matches the packaged systemd service configuration.
.SH EXAMPLES
Send a comment to pull request 42 of example/repo:
.PP
.RS
.nf
comenq example/repo 42 "Queued for review"
.fi
.RE
.SH SEE ALSO
.BR comenqd (8)
44 changes: 44 additions & 0 deletions packaging/man/comenqd.1
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
.TH COMENQD 8 "May 2024" "comenqd" "System Administration"
.SH NAME
comenqd \- daemon that posts queued GitHub pull request comments
.SH SYNOPSIS
.B comenqd
[
.B --config
.I file
]
[
.B --github-token
.I token
]
[
.B --socket-path
.I path
]
[
.B --queue-path
.I path
]
.SH DESCRIPTION
The
.B comenqd
daemon consumes comment requests queued by the
.BR comenq (1)
CLI and posts them to GitHub. Configuration is loaded from
.I /etc/comenqd/config.toml
and may be overridden through command line options or
environment variables prefixed with
.BR COMENQD_.
.PP
The daemon is designed to run as a system service using the packaged
systemd unit. When launched manually it logs its effective socket and
queue locations to the terminal.
.SH FILES
.TP
.I /etc/comenqd/config.toml
Default configuration file.
.TP
.I /var/lib/comenq/queue
On-disk queue storage used for persistence across restarts.
.SH SEE ALSO
.BR comenq (1)
2 changes: 1 addition & 1 deletion test-support/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pub mod daemon;
pub mod env_guard;
pub mod util;
mod workflow;
pub use workflow::uses_goreleaser;
pub use workflow::uses_shared_release_actions;

// Re-exports from daemon module (added in main)
pub use daemon::{TestConfig, octocrab_for, temp_config};
Expand Down
60 changes: 43 additions & 17 deletions test-support/src/workflow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@

use serde_yaml::Value;

/// Return `true` if the workflow steps include the `GoReleaser` action.
/// Return `true` when the release workflow uses the shared composite actions to
/// build binaries and publish packages.
///
/// # Errors
///
/// Returns an error if the YAML cannot be parsed.
pub fn uses_goreleaser(yaml: &str) -> Result<bool, serde_yaml::Error> {
pub fn uses_shared_release_actions(yaml: &str) -> Result<bool, serde_yaml::Error> {
let doc: Value = serde_yaml::from_str(yaml)?;
let Some(jobs) = doc.get("jobs") else {
return Ok(false);
};
let Some(map) = jobs.as_mapping() else {
return Ok(false);
};

let mut saw_rust_builder = false;
let mut saw_release_publisher = false;
for job in map.values() {
let Some(steps) = job.get("steps") else {
continue;
Expand All @@ -23,38 +27,60 @@ pub fn uses_goreleaser(yaml: &str) -> Result<bool, serde_yaml::Error> {
continue;
};
for step in arr {
if step
.get("uses")
.and_then(|u| u.as_str())
.is_some_and(|s| s.starts_with("goreleaser/goreleaser-action"))
{
return Ok(true);
if let Some(uses) = step.get("uses").and_then(Value::as_str) {
if uses
== "leynos/shared-actions/.github/actions/rust-build-release@7bc9b6c15964ef98733aa647b76d402146284ba3"
{
saw_rust_builder = true;
}
if uses.starts_with("softprops/action-gh-release@") {
saw_release_publisher = true;
}
}
}
}
Ok(false)

Ok(saw_rust_builder && saw_release_publisher)
}

#[cfg(test)]
mod tests {
use super::uses_goreleaser;
use super::uses_shared_release_actions;

#[test]
#[expect(clippy::expect_used, reason = "simplify test output")]
fn detects_shared_actions() {
let yaml = r"
jobs:
release:
steps:
- uses: leynos/shared-actions/.github/actions/rust-build-release@7bc9b6c15964ef98733aa647b76d402146284ba3
- uses: softprops/action-gh-release@v2
";
assert!(uses_shared_release_actions(yaml).expect("parse"));
}

#[test]
#[expect(clippy::expect_used, reason = "simplify test output")]
fn detects_goreleaser() {
fn missing_builder_fails() {
let yaml = r"
jobs:
goreleaser:
release:
steps:
- uses: goreleaser/goreleaser-action@v5
- uses: softprops/action-gh-release@v2
";
assert!(uses_goreleaser(yaml).expect("parse"));
assert!(!uses_shared_release_actions(yaml).expect("parse"));
}

#[test]
#[expect(clippy::expect_used, reason = "simplify test output")]
fn missing_goreleaser() {
let yaml = "jobs: {}";
assert!(!uses_goreleaser(yaml).expect("parse"));
fn missing_publisher_fails() {
let yaml = r"
jobs:
release:
steps:
- uses: leynos/shared-actions/.github/actions/rust-build-release@7bc9b6c15964ef98733aa647b76d402146284ba3
";
assert!(!uses_shared_release_actions(yaml).expect("parse"));
}
}
4 changes: 2 additions & 2 deletions tests/features/release.feature
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
Feature: Release workflow

Scenario: goreleaser step present
Scenario: shared release actions present
Given the release workflow file
When it is parsed as YAML
Then the workflow uses goreleaser
Then the workflow uses the shared release actions

Scenario: triggers on version tags
Given the release workflow file
Expand Down
Loading
Loading