diff --git a/.github/actions/build-appimage/action.yml b/.github/actions/build-appimage/action.yml new file mode 100644 index 0000000..339c2a6 --- /dev/null +++ b/.github/actions/build-appimage/action.yml @@ -0,0 +1,127 @@ +name: 'Build AppImage' +description: > + Packages a self-contained .NET binary into an AppImage for linux-x64. + Requires the repository to be checked out and the binary to exist at the given path. + Note: FUSE is not available on GitHub-hosted runners, so appimagetool runs via + --appimage-extract-and-run. + +inputs: + binary-path: + description: 'Path to the compiled binary to package' + required: true + version: + description: 'Application version (no v-prefix; may contain + for nightly builds)' + required: true + is-nightly: + description: 'Set to "true" to replace + with ~ in the AppStream version' + required: false + default: 'false' + asset-prefix: + description: 'Filename prefix for the output AppImage' + required: false + default: 'OpenSSH-GUI' + appimagetool-tag: + description: 'Release tag of appimagetool to download' + required: false + default: 'continuous' + +outputs: + appimage-name: + description: 'Filename of the produced AppImage' + value: ${{ steps.build.outputs.appimage-name }} + +runs: + using: composite + steps: + - name: Install AppImage build dependencies + shell: bash + run: | + set -euo pipefail + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends librsvg2-bin appstream + + - name: Download appimagetool + shell: bash + run: | + set -euo pipefail + TOOL_URL="https://github.com/AppImage/appimagetool/releases/download/${{ inputs.appimagetool-tag }}/appimagetool-x86_64.AppImage" + echo "::notice::Downloading appimagetool from $TOOL_URL" + wget --progress=dot:giga "$TOOL_URL" -O appimagetool + chmod +x appimagetool + + - name: Assemble and build AppImage + id: build + shell: bash + run: | + set -euo pipefail + + BINARY_PATH="${{ inputs.binary-path }}" + VERSION="${{ inputs.version }}" + IS_NIGHTLY="${{ inputs.is-nightly }}" + ASSET_PREFIX="${{ inputs.asset-prefix }}" + APP_ID="io.github.frequency403.openssh_gui" + + # Validate all required source files exist before doing any work + for REQUIRED in \ + "$BINARY_PATH" \ + "images/openssh-gui.svg" \ + "appimage/${APP_ID}.metainfo.xml" \ + "appimage/AppRun" + do + if [[ ! -f "$REQUIRED" ]]; then + echo "::error::Required file not found: $REQUIRED" + exit 1 + fi + done + + # Prepare AppDir layout + mkdir -p "AppDir/usr/bin" + mkdir -p "AppDir/usr/share/icons/hicolor/256x256/apps" + mkdir -p "AppDir/usr/share/icons/hicolor/scalable/apps" + mkdir -p "AppDir/usr/share/applications" + mkdir -p "AppDir/usr/share/metainfo" + + cp "$BINARY_PATH" AppDir/usr/bin/openssh-gui + chmod +x AppDir/usr/bin/openssh-gui + + # Icons + rsvg-convert -w 256 -h 256 images/openssh-gui.svg \ + -o AppDir/usr/share/icons/hicolor/256x256/apps/openssh-gui.png + cp images/openssh-gui.svg \ + AppDir/usr/share/icons/hicolor/scalable/apps/openssh-gui.svg + cp AppDir/usr/share/icons/hicolor/256x256/apps/openssh-gui.png AppDir/openssh-gui.png + cp AppDir/usr/share/icons/hicolor/256x256/apps/openssh-gui.png AppDir/appicon.png + + # Metainfo: patch placeholder version and date + METAINFO_DEST="AppDir/usr/share/metainfo/${APP_ID}.metainfo.xml" + cp "appimage/${APP_ID}.metainfo.xml" "$METAINFO_DEST" + + APPSTREAM_VERSION="$VERSION" + if [[ "$IS_NIGHTLY" == "true" ]]; then + APPSTREAM_VERSION="${APPSTREAM_VERSION//+/~}" + fi + sed -i \ + "s|||" \ + "$METAINFO_DEST" + + # Generate .desktop file from metainfo + appstreamcli make-desktop-file \ + "$METAINFO_DEST" \ + "AppDir/usr/share/applications/${APP_ID}.desktop" + cp "AppDir/usr/share/applications/${APP_ID}.desktop" \ + "AppDir/${APP_ID}.desktop" + + cp appimage/AppRun AppDir/AppRun + chmod +x AppDir/AppRun + + # Assemble AppImage (--appimage-extract-and-run bypasses FUSE requirement) + APPIMAGE_NAME="${ASSET_PREFIX}-x86_64.AppImage" + ARCH=x86_64 ./appimagetool --appimage-extract-and-run AppDir "$APPIMAGE_NAME" + + if [[ ! -f "$APPIMAGE_NAME" ]]; then + echo "::error::appimagetool did not produce $APPIMAGE_NAME" + exit 1 + fi + + echo "appimage-name=$APPIMAGE_NAME" >> "$GITHUB_OUTPUT" + echo "::notice::AppImage built successfully: $APPIMAGE_NAME" \ No newline at end of file diff --git a/.github/actions/determine-version/action.yml b/.github/actions/determine-version/action.yml new file mode 100644 index 0000000..573f13a --- /dev/null +++ b/.github/actions/determine-version/action.yml @@ -0,0 +1,43 @@ +name: Determine Version +description: Extract and validate a semantic version from Directory.Build.props and optionally validate against a branch or tag name + +outputs: + version: + description: Extracted semantic version without leading v + value: ${{ steps.extract.outputs.version }} + tag: + description: Tag name with leading v + value: ${{ steps.extract.outputs.tag }} + +runs: + using: composite + + steps: + - name: Extract BaseVersion + id: extract + shell: bash + run: | + set -euo pipefail + + FILE="Directory.Build.props" + + if [[ ! -f "$FILE" ]]; then + echo "::error::$FILE not found" + exit 1 + fi + + VERSION="$(dotnet msbuild "$FILE" -nologo -getProperty:BaseVersion | tr -d '\r')" + + if [[ -z "$VERSION" ]]; then + echo "::error::BaseVersion not found in $FILE" + exit 1 + fi + + if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Invalid BaseVersion: $VERSION" + exit 1 + fi + + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=v$VERSION" >> "$GITHUB_OUTPUT" + echo "::notice::Detected version: $VERSION (tag: v$VERSION)" \ No newline at end of file diff --git a/.github/actions/dotnet-publish/action.yml b/.github/actions/dotnet-publish/action.yml new file mode 100644 index 0000000..5ee1116 --- /dev/null +++ b/.github/actions/dotnet-publish/action.yml @@ -0,0 +1,40 @@ +name: 'Publish .NET Application' +description: > + Runs dotnet publish for a single-file, ReadyToRun, self-contained Release build. + Assumes the .NET SDK is already set up in the environment. + +inputs: + project-path: + description: 'Relative path to the .csproj file to publish' + required: true + runtime: + description: 'Target RID (e.g. win-x64, linux-x64, osx-x64)' + required: true + version: + description: 'Version string passed to MSBuild /p:Version (no v-prefix)' + required: true + output-dir: + description: 'Output directory relative to the workspace root' + required: false + default: './publish' + extra-msbuild-args: + description: 'Additional MSBuild property flags, e.g. "-p:IsNightly=true"' + required: false + default: '' + +runs: + using: composite + steps: + - name: Publish application + shell: bash + run: | + set -euo pipefail + dotnet publish "${{ inputs.project-path }}" \ + --configuration Release \ + --runtime "${{ inputs.runtime }}" \ + --output "${{ inputs.output-dir }}" \ + -p:PublishSingleFile=true \ + -p:PublishReadyToRun=true \ + -p:IncludeNativeLibrariesForSelfExtract=true \ + -p:Version="${{ inputs.version }}" \ + ${{ inputs.extra-msbuild-args }} \ No newline at end of file diff --git a/.github/actions/dotnet-setup/action.yml b/.github/actions/dotnet-setup/action.yml new file mode 100644 index 0000000..ac20840 --- /dev/null +++ b/.github/actions/dotnet-setup/action.yml @@ -0,0 +1,59 @@ +name: 'Setup .NET Environment' +description: > + Checks out the repository, auto-detects the TargetFramework from + Directory.Build.props, sets up the .NET SDK, and restores the NuGet cache. + +inputs: + fetch-depth: + description: 'Number of commits to fetch (0 = full history, 1 = shallow)' + required: false + default: '1' + token: + description: 'GitHub token used for checkout' + required: false + default: ${{ github.token }} + +outputs: + dotnet-version: + description: 'Resolved .NET version string, e.g. "10.0.x"' + value: ${{ steps.detect-tfm.outputs.version }} + +runs: + using: composite + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: ${{ inputs.fetch-depth }} + token: ${{ inputs.token }} + + - name: Detect TargetFramework from Directory.Build.props + id: detect-tfm + shell: bash + run: | + set -euo pipefail + PROPS_FILE="Directory.Build.props" + if [[ ! -f "$PROPS_FILE" ]]; then + echo "::error::$PROPS_FILE not found in workspace root" + exit 1 + fi + TFM=$(grep -oPm1 '(?<=net)[0-9.]+' "$PROPS_FILE" || true) + if [[ -z "$TFM" ]]; then + echo "::error::Could not extract from $PROPS_FILE" + exit 1 + fi + echo "version=${TFM}.x" >> "$GITHUB_OUTPUT" + echo "::notice::Resolved .NET version: ${TFM}.x" + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ steps.detect-tfm.outputs.version }} + + - name: Restore NuGet cache + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/Directory.Packages.props', '**/Directory.Build.props') }} + restore-keys: | + ${{ runner.os }}-nuget- \ No newline at end of file diff --git a/.github/actions/install-komac/action.yml b/.github/actions/install-komac/action.yml new file mode 100644 index 0000000..84cd3a0 --- /dev/null +++ b/.github/actions/install-komac/action.yml @@ -0,0 +1,37 @@ +name: 'Install Komac' +description: > + Resolves and downloads the latest Komac (linux-amd64) release from GitHub + and makes it executable in the current working directory. + +outputs: + komac-path: + description: 'Absolute path to the komac binary' + value: ${{ steps.install.outputs.komac-path }} + +runs: + using: composite + steps: + - name: Resolve and download Komac + id: install + shell: bash + run: | + set -euo pipefail + + KOMAC_URL=$(curl -fsSL \ + https://api.github.com/repos/russellbanks/Komac/releases/latest \ + | grep -o '"browser_download_url": "[^"]*linux-amd64[^"]*"' \ + | grep -v '\.sha' \ + | head -1 \ + | cut -d'"' -f4) + + if [[ -z "$KOMAC_URL" ]]; then + echo "::error::Failed to resolve Komac download URL from GitHub Releases API" + exit 1 + fi + + echo "::notice::Downloading Komac from: $KOMAC_URL" + curl -fsSL "$KOMAC_URL" -o komac + chmod +x komac + ./komac --version + + echo "komac-path=$(pwd)/komac" >> "$GITHUB_OUTPUT" \ No newline at end of file diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml index aac7269..2e68ff3 100644 --- a/.github/workflows/auto-tag.yml +++ b/.github/workflows/auto-tag.yml @@ -10,12 +10,17 @@ on: permissions: contents: write +concurrency: + group: auto-tag-${{ github.ref }} + cancel-in-progress: false + jobs: create-tag: name: Create and Push Tag # Only run when a release/* branch was actually merged (not just closed) if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/') runs-on: ubuntu-latest + timeout-minutes: 5 steps: - name: Checkout repository @@ -24,25 +29,17 @@ jobs: fetch-depth: 0 token: ${{ secrets.PAT_TOKEN }} - - name: Extract version from branch name + - name: Extract and validate version from branch name id: version - run: | - BRANCH="${{ github.event.pull_request.head.ref }}" - # Strip 'release/' prefix - VERSION="${BRANCH#release/}" - # Strip optional 'v' prefix - VERSION="${VERSION#v}" - - echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" - echo "TAG=v${VERSION}" >> "$GITHUB_OUTPUT" - echo "::notice ::Detected version: $VERSION (tag: v${VERSION})" + uses: ./.github/actions/determine-version - name: Check if tag already exists id: check run: | + set -euo pipefail if git rev-parse "refs/tags/${{ steps.version.outputs.TAG }}" >/dev/null 2>&1; then - echo "exists=true" >> "$GITHUB_OUTPUT" - echo "::warning ::Tag ${{ steps.version.outputs.TAG }} already exists, skipping." + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "::warning::Tag ${{ steps.version.outputs.TAG }} already exists, skipping." else echo "exists=false" >> "$GITHUB_OUTPUT" fi @@ -50,8 +47,10 @@ jobs: - name: Create and push tag if: steps.check.outputs.exists == 'false' run: | - git config user.name "github-actions[bot]" + set -euo pipefail + git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git tag -a "${{ steps.version.outputs.TAG }}" -m "Release ${{ steps.version.outputs.VERSION }}" + git tag -a "${{ steps.version.outputs.TAG }}" \ + -m "Release ${{ steps.version.outputs.VERSION }}" git push origin "${{ steps.version.outputs.TAG }}" - echo "::notice ::Tag ${{ steps.version.outputs.TAG }} pushed successfully." \ No newline at end of file + echo "::notice::Tag ${{ steps.version.outputs.TAG }} pushed successfully." \ No newline at end of file diff --git a/.github/workflows/build-and-package.yml b/.github/workflows/build-and-package.yml index 28f9309..b058645 100644 --- a/.github/workflows/build-and-package.yml +++ b/.github/workflows/build-and-package.yml @@ -13,13 +13,15 @@ on: asset_name_prefix: required: false type: string - default: "OpenSSH-GUI" + default: 'OpenSSH-GUI' jobs: build: name: Build for ${{ matrix.target }} runs-on: ubuntu-latest + timeout-minutes: 30 strategy: + fail-fast: false # One failing target must not kill the others matrix: include: - target: linux-x64 @@ -30,31 +32,17 @@ jobs: asset_extension: '' steps: - - name: Checkout repository + - name: Checkout Repository uses: actions/checkout@v4 + + - name: Setup .NET environment + uses: ./.github/actions/dotnet-setup - - name: Determine .NET version - id: dotnet-version - run: | - TFM=$(grep -oPm1 '(?<=net)[0-9.]+' Directory.Build.props) - echo "version=${TFM}.x" >> "$GITHUB_OUTPUT" - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: ${{ steps.dotnet-version.outputs.version }} - - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: ~/.nuget/packages - key: ${{ runner.os }}-dotnet-${{ hashFiles('**/*.csproj', '**/Directory.Packages.props', '**/Directory.Build.props') }} - restore-keys: | - ${{ runner.os }}-dotnet- - - - name: Normalize version and build args - id: build-meta + - name: Normalize version and build flags + id: meta + shell: bash run: | + set -euo pipefail VERSION="${{ inputs.version }}" VERSION="${VERSION#v}" echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" @@ -66,95 +54,65 @@ jobs: echo "EXTRA_ARGS=$EXTRA_ARGS" >> "$GITHUB_OUTPUT" - name: Publish application - run: | - dotnet publish OpenSSH_GUI/OpenSSH_GUI.csproj \ - --configuration Release \ - --runtime ${{ matrix.target }} \ - --output "./publish" \ - -p:PublishSingleFile=true \ - -p:PublishReadyToRun=true \ - -p:IncludeNativeLibrariesForSelfExtract=true \ - -p:Version="${{ steps.build-meta.outputs.VERSION }}" \ - ${{ steps.build-meta.outputs.EXTRA_ARGS }} + uses: ./.github/actions/dotnet-publish + with: + project-path: OpenSSH_GUI/OpenSSH_GUI.csproj + runtime: ${{ matrix.target }} + version: ${{ steps.meta.outputs.VERSION }} + output-dir: ./publish + extra-msbuild-args: ${{ steps.meta.outputs.EXTRA_ARGS }} - - name: Rename artifact + - name: Rename artifact for distribution id: rename + shell: bash run: | + set -euo pipefail ASSET_NAME="${{ inputs.asset_name_prefix }}-${{ matrix.target }}${{ matrix.asset_extension }}" - mv ./publish/OpenSSH_GUI${{ matrix.asset_extension }} "./publish/$ASSET_NAME" + SOURCE="./publish/OpenSSH_GUI${{ matrix.asset_extension }}" + if [[ ! -f "$SOURCE" ]]; then + echo "::error::Expected build output not found: $SOURCE" + exit 1 + fi + mv "$SOURCE" "./publish/$ASSET_NAME" echo "ASSET_NAME=$ASSET_NAME" >> "$GITHUB_OUTPUT" - # --- AppImage (Linux only) --- - name: Build AppImage if: matrix.target == 'linux-x64' id: appimage - run: | - sudo apt-get update && sudo apt-get install -y librsvg2-bin - - wget -q https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O appimagetool - chmod +x appimagetool - - mkdir -p AppDir/usr/bin - mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps - mkdir -p AppDir/usr/share/icons/hicolor/scalable/apps - mkdir -p AppDir/usr/share/applications - mkdir -p AppDir/usr/share/metainfo - - cp "./publish/${{ steps.rename.outputs.ASSET_NAME }}" AppDir/usr/bin/openssh-gui - chmod +x AppDir/usr/bin/openssh-gui - - rsvg-convert -w 256 -h 256 images/openssh-gui.svg -o AppDir/usr/share/icons/hicolor/256x256/apps/openssh-gui.png - cp images/openssh-gui.svg AppDir/usr/share/icons/hicolor/scalable/apps/openssh-gui.svg - cp AppDir/usr/share/icons/hicolor/256x256/apps/openssh-gui.png AppDir/openssh-gui.png - cp AppDir/usr/share/icons/hicolor/256x256/apps/openssh-gui.png AppDir/appicon.png - - cp appimage/io.github.frequency403.openssh_gui.metainfo.xml AppDir/usr/share/metainfo/io.github.frequency403.openssh_gui.metainfo.xml - - APPSTREAM_VERSION="${{ steps.build-meta.outputs.VERSION }}" - if [[ "${{ inputs.is_nightly }}" == "true" ]]; then - APPSTREAM_VERSION="${APPSTREAM_VERSION//+/~}" - fi - - sed -i "s|||" \ - AppDir/usr/share/metainfo/io.github.frequency403.openssh_gui.metainfo.xml - - appstreamcli make-desktop-file \ - AppDir/usr/share/metainfo/io.github.frequency403.openssh_gui.metainfo.xml \ - AppDir/usr/share/applications/io.github.frequency403.openssh_gui.desktop - - cp AppDir/usr/share/applications/io.github.frequency403.openssh_gui.desktop \ - AppDir/io.github.frequency403.openssh_gui.desktop - - cp appimage/AppRun AppDir/AppRun - chmod +x AppDir/AppRun - - APPIMAGE_NAME="${{ inputs.asset_name_prefix }}-x86_64.AppImage" - ARCH=x86_64 ./appimagetool --appimage-extract-and-run AppDir "$APPIMAGE_NAME" - echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> "$GITHUB_OUTPUT" + uses: ./.github/actions/build-appimage + with: + binary-path: ./publish/${{ steps.rename.outputs.ASSET_NAME }} + version: ${{ steps.meta.outputs.VERSION }} + is-nightly: ${{ inputs.is_nightly }} + asset-prefix: ${{ inputs.asset_name_prefix }} - - name: Upload AppImage artifact + - name: Upload AppImage if: matrix.target == 'linux-x64' uses: actions/upload-artifact@v4 with: - name: ${{ steps.appimage.outputs.APPIMAGE_NAME }} - path: ${{ steps.appimage.outputs.APPIMAGE_NAME }} + name: ${{ steps.appimage.outputs.appimage-name }} + path: ${{ steps.appimage.outputs.appimage-name }} + if-no-files-found: error - - name: Upload generated desktop file + - name: Upload .desktop file if: matrix.target == 'linux-x64' uses: actions/upload-artifact@v4 with: name: io.github.frequency403.openssh_gui.desktop path: AppDir/usr/share/applications/io.github.frequency403.openssh_gui.desktop + if-no-files-found: error - - name: Upload appicon artifact + - name: Upload app icon if: matrix.target == 'linux-x64' uses: actions/upload-artifact@v4 with: name: appicon path: AppDir/appicon.png + if-no-files-found: error - - name: Upload build artifact + - name: Upload platform binary uses: actions/upload-artifact@v4 with: name: ${{ steps.rename.outputs.ASSET_NAME }} - path: ./publish/${{ steps.rename.outputs.ASSET_NAME }} \ No newline at end of file + path: ./publish/${{ steps.rename.outputs.ASSET_NAME }} + if-no-files-found: error \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index be9449c..96f6419 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,26 +3,43 @@ on: push: tags: - - 'v[0-9]*.[0-9]*.[0-9]*' # Trigger only on version tags like v1.2.3 + - 'v[0-9]*.[0-9]*.[0-9]*' permissions: contents: write +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false # Never cancel an in-flight release + jobs: - # --- JOB 1: BUILD --- + prepare: + name: Prepare Metadata + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + tag: ${{ steps.version.outputs.tag }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Determine Version + id: version + uses: ./.github/actions/determine-version + build: + needs: prepare uses: ./.github/workflows/build-and-package.yml with: - version: ${{ github.ref_name }} + version: ${{ needs.prepare.outputs.version }} is_nightly: false asset_name_prefix: OpenSSH-GUI - # --- JOB 2: RELEASE --- release: name: Create GitHub Release runs-on: ubuntu-latest - needs: build - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + timeout-minutes: 15 + needs: [ prepare, build ] steps: - name: Checkout repository @@ -33,70 +50,104 @@ jobs: with: path: artifacts/ - - name: Flatten and Prepare Assets + - name: Prepare and validate release assets run: | + set -euo pipefail rm -rf release-assets mkdir -p release-assets - find artifacts -type f -exec cp {} release-assets/ \; + # Copy all artifacts, hard-fail on name collision + find artifacts -type f | while read -r FILE; do + DEST="release-assets/$(basename "$FILE")" + if [[ -f "$DEST" ]]; then + echo "::error::Artifact name collision detected: $(basename "$FILE")" + exit 1 + fi + cp "$FILE" "$DEST" + done cp LICENSE release-assets/LICENSE - echo "Release Assets:" - ls -la release-assets - - - name: Create Release and Upload Assets + # Assert all expected files are present before creating the release + for EXPECTED in \ + "OpenSSH-GUI-linux-x64" \ + "OpenSSH-GUI-win-x64.exe" \ + "OpenSSH-GUI-osx-x64" \ + "OpenSSH-GUI-x86_64.AppImage" + do + if [[ ! -f "release-assets/$EXPECTED" ]]; then + echo "::error::Expected release asset missing: $EXPECTED" + exit 1 + fi + done + + echo "Release assets:" + ls -lah release-assets/ + + - name: Publish GitHub Release uses: softprops/action-gh-release@v2 with: - tag_name: ${{ github.ref_name }} - files: "release-assets/*" + tag_name: ${{ needs.prepare.outputs.tag }} + files: release-assets/* generate_release_notes: true deploy-aur: name: Update AUR Packages runs-on: ubuntu-latest - needs: release - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + timeout-minutes: 15 + needs: [ prepare, release ] + steps: - - name: Checkout Repository + - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Download Artifacts + - name: Download build artifacts uses: actions/download-artifact@v4 with: path: artifacts/ - - name: Prepare for PKGBUILD update + - name: Prepare AUR assets and update PKGBUILDs run: | + set -euo pipefail rm -rf aur-assets mkdir -p aur-assets - find artifacts -type f -exec cp {} aur-assets/ \; - - echo "AUR asset files:" - ls -la aur-assets - - test -f aur-assets/OpenSSH-GUI-linux-x64 - test -f aur-assets/appicon.png - test -f aur-assets/io.github.frequency403.openssh_gui.desktop - test -f LICENSE - - VERSION=${GITHUB_REF_NAME#v} - SHA_BIN=$(sha256sum aur-assets/OpenSSH-GUI-linux-x64 | cut -d' ' -f1) - SHA_ICON=$(sha256sum aur-assets/appicon.png | cut -d' ' -f1) - SHA_DESKTOP=$(sha256sum aur-assets/io.github.frequency403.openssh_gui.desktop | cut -d' ' -f1) - SHA_LICENSE=$(sha256sum LICENSE | cut -d' ' -f1) + find artifacts -type f | while read -r FILE; do + DEST="aur-assets/$(basename "$FILE")" + if [[ -f "$DEST" ]]; then + echo "::error::AUR artifact name collision: $(basename "$FILE")" + exit 1 + fi + cp "$FILE" "$DEST" + done + + # Assert all required AUR files are present + test -f aur-assets/OpenSSH-GUI-linux-x64 \ + || { echo "::error::Missing linux binary"; exit 1; } + test -f aur-assets/appicon.png \ + || { echo "::error::Missing appicon.png"; exit 1; } + test -f "aur-assets/io.github.frequency403.openssh_gui.desktop" \ + || { echo "::error::Missing .desktop file"; exit 1; } + test -f LICENSE \ + || { echo "::error::Missing LICENSE"; exit 1; } + + VERSION="${{ needs.prepare.outputs.version }}" + SHA_BIN=$(sha256sum aur-assets/OpenSSH-GUI-linux-x64 | cut -d' ' -f1) + SHA_ICON=$(sha256sum aur-assets/appicon.png | cut -d' ' -f1) + SHA_DESKTOP=$(sha256sum "aur-assets/io.github.frequency403.openssh_gui.desktop" | cut -d' ' -f1) + SHA_LICENSE=$(sha256sum LICENSE | cut -d' ' -f1) sed -i "s/^pkgver=.*/pkgver=$VERSION/" openssh-gui-bin/PKGBUILD - sed -i "s/^sha256sums=.*/sha256sums=('$SHA_BIN' '$SHA_ICON' '$SHA_DESKTOP' '$SHA_LICENSE')/" openssh-gui-bin/PKGBUILD + sed -i "s/^sha256sums=.*/sha256sums=('$SHA_BIN' '$SHA_ICON' '$SHA_DESKTOP' '$SHA_LICENSE')/" \ + openssh-gui-bin/PKGBUILD sed -i "s/^pkgver=.*/pkgver=$VERSION/" openssh-gui-git/PKGBUILD - echo "Updated openssh-gui-bin PKGBUILD:" + echo "Updated PKGBUILDs:" grep -E '^(pkgver=|sha256sums=)' openssh-gui-bin/PKGBUILD - - name: Update AUR (openssh-gui-bin) + - name: Publish AUR (openssh-gui-bin) uses: KSXGitHub/github-actions-deploy-aur@v4.1.1 with: pkgname: openssh-gui-bin @@ -104,9 +155,9 @@ jobs: commit_username: ${{ github.repository_owner }} commit_email: ${{ github.repository_owner }}@users.noreply.github.com ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} - commit_message: "Update to ${{ github.ref_name }}" + commit_message: "Update to ${{ needs.prepare.outputs.tag }}" - - name: Update AUR (openssh-gui-git) + - name: Publish AUR (openssh-gui-git) uses: KSXGitHub/github-actions-deploy-aur@v4.1.1 with: pkgname: openssh-gui-git @@ -114,32 +165,27 @@ jobs: commit_username: ${{ github.repository_owner }} commit_email: ${{ github.repository_owner }}@users.noreply.github.com ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} - commit_message: "Update to ${{ github.ref_name }}" - + commit_message: "Update to ${{ needs.prepare.outputs.tag }}" + winget: name: Update Winget Package runs-on: ubuntu-latest - needs: release - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') - - steps: - - name: Extract version from tag - id: version - run: | - VERSION="${GITHUB_REF_NAME#v}" - echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" + timeout-minutes: 10 + needs: [ prepare, release ] + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + - name: Install Komac - run: | - curl -sL \ - "https://github.com/russellbanks/Komac/releases/latest/download/komac-linux-amd64" \ - -o komac - chmod +x komac + id: komac + uses: ./.github/actions/install-komac - name: Update Winget manifest run: | - ./komac update "frequency403.OpenSSHGUI" \ - --version "${{ steps.version.outputs.VERSION }}" \ - --urls "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/OpenSSH-GUI-win-x64.exe" \ + set -euo pipefail + "${{ steps.komac.outputs.komac-path }}" update "frequency403.OpenSSHGUI" \ + --version "${{ needs.prepare.outputs.version }}" \ + --urls "https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare.outputs.tag }}/OpenSSH-GUI-win-x64.exe" \ --submit \ --token "${{ secrets.WINGET_GITHUB_TOKEN }}" \ No newline at end of file diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 8a50dbf..3591675 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -8,32 +8,44 @@ on: permissions: contents: write +concurrency: + group: nightly-${{ github.ref }} + cancel-in-progress: true # Supersede previous nightly on rapid pushes + jobs: - # --- JOB 0: PREPARE --- prepare: name: Prepare Metadata runs-on: ubuntu-latest + timeout-minutes: 5 outputs: version: ${{ steps.meta.outputs.version }} base_version: ${{ steps.meta.outputs.base_version }} git_hash: ${{ steps.meta.outputs.git_hash }} build_date: ${{ steps.meta.outputs.build_date }} + steps: - - name: Checkout + - name: Checkout repository uses: actions/checkout@v4 - - name: Resolve metadata + + - name: Determine Version + id: version + uses: ./.github/actions/determine-version + + - name: Resolve build metadata id: meta run: | + set -euo pipefail HASH=$(git rev-parse --short HEAD) DATE=$(date +%Y-%m-%d) - BASE_VERSION=$(grep -oPm1 '(?<=)[^<]+' Directory.Build.props) + BASE_VERSION="${{ steps.version.outputs.version }}" + VERSION="${BASE_VERSION}+${HASH}" - echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "base_version=$BASE_VERSION" >> "$GITHUB_OUTPUT" - echo "git_hash=$HASH" >> "$GITHUB_OUTPUT" - echo "build_date=$DATE" >> "$GITHUB_OUTPUT" + echo "git_hash=$HASH" >> "$GITHUB_OUTPUT" + echo "build_date=$DATE" >> "$GITHUB_OUTPUT" + echo "::notice::Nightly version: $VERSION" - # --- JOB 1: BUILD --- build: needs: prepare uses: ./.github/workflows/build-and-package.yml @@ -45,52 +57,61 @@ jobs: deploy-aur-nightly: name: Update AUR Nightly Package runs-on: ubuntu-latest + timeout-minutes: 15 needs: [ prepare, build ] - + steps: - - name: Checkout Repository + - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Download Artifacts + - name: Download build artifacts uses: actions/download-artifact@v4 with: path: artifacts/ - - name: Prepare for PKGBUILD update + - name: Prepare and update nightly PKGBUILD run: | set -euo pipefail - rm -rf aur-assets mkdir -p aur-assets - - find artifacts -type f -exec cp {} aur-assets/ \; - - echo "AUR asset files:" - find aur-assets -maxdepth 1 -type f -printf '%f\n' | sort - - test -f aur-assets/OpenSSH-GUI-nightly-linux-x64 - test -f aur-assets/appicon.png - test -f aur-assets/io.github.frequency403.openssh_gui.desktop - test -f LICENSE - + + find artifacts -type f | while read -r FILE; do + DEST="aur-assets/$(basename "$FILE")" + if [[ -f "$DEST" ]]; then + echo "::error::AUR artifact name collision: $(basename "$FILE")" + exit 1 + fi + cp "$FILE" "$DEST" + done + + test -f aur-assets/OpenSSH-GUI-nightly-linux-x64 \ + || { echo "::error::Missing nightly linux binary"; exit 1; } + test -f aur-assets/appicon.png \ + || { echo "::error::Missing appicon.png"; exit 1; } + test -f "aur-assets/io.github.frequency403.openssh_gui.desktop" \ + || { echo "::error::Missing .desktop file"; exit 1; } + test -f LICENSE \ + || { echo "::error::Missing LICENSE"; exit 1; } + DATE=$(date +%Y%m%d) - VERSION="${{ needs.prepare.outputs.base_version }}.${DATE}.${{ needs.prepare.outputs.git_hash }}" - echo "AUR_VERSION=$VERSION" >> "$GITHUB_ENV" - - SHA_BIN=$(sha256sum aur-assets/OpenSSH-GUI-nightly-linux-x64 | cut -d' ' -f1) - SHA_ICON=$(sha256sum aur-assets/appicon.png | cut -d' ' -f1) - SHA_DESKTOP=$(sha256sum aur-assets/io.github.frequency403.openssh_gui.desktop | cut -d' ' -f1) - SHA_LICENSE=$(sha256sum LICENSE | cut -d' ' -f1) - - sed -i "s/^pkgver=.*/pkgver=$VERSION/" openssh-gui-nightly/PKGBUILD - sed -i "s/^sha256sums=.*/sha256sums=('$SHA_BIN' '$SHA_ICON' '$SHA_DESKTOP' '$SHA_LICENSE')/" openssh-gui-nightly/PKGBUILD - - echo "Updated openssh-gui-nightly PKGBUILD:" + AUR_VERSION="${{ needs.prepare.outputs.base_version }}.${DATE}.${{ needs.prepare.outputs.git_hash }}" + echo "AUR_VERSION=$AUR_VERSION" >> "$GITHUB_ENV" + + SHA_BIN=$(sha256sum aur-assets/OpenSSH-GUI-nightly-linux-x64 | cut -d' ' -f1) + SHA_ICON=$(sha256sum aur-assets/appicon.png | cut -d' ' -f1) + SHA_DESKTOP=$(sha256sum "aur-assets/io.github.frequency403.openssh_gui.desktop" | cut -d' ' -f1) + SHA_LICENSE=$(sha256sum LICENSE | cut -d' ' -f1) + + sed -i "s/^pkgver=.*/pkgver=$AUR_VERSION/" openssh-gui-nightly/PKGBUILD + sed -i "s/^sha256sums=.*/sha256sums=('$SHA_BIN' '$SHA_ICON' '$SHA_DESKTOP' '$SHA_LICENSE')/" \ + openssh-gui-nightly/PKGBUILD + + echo "Updated PKGBUILD:" grep -E '^(pkgver=|sha256sums=)' openssh-gui-nightly/PKGBUILD - - name: Update AUR (openssh-gui-nightly) + - name: Publish AUR (openssh-gui-nightly) uses: KSXGitHub/github-actions-deploy-aur@v4.1.1 with: pkgname: openssh-gui-nightly diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0d80e4a..202c2c6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,39 +6,28 @@ on: pull_request: branches: [ main, master, development ] +concurrency: + group: tests-${{ github.ref }}-${{ github.event_name }} + cancel-in-progress: true + jobs: test: name: Run Tests runs-on: ubuntu-latest + timeout-minutes: 20 steps: - name: Checkout repository uses: actions/checkout@v4 - - - name: Determine .NET version from project - id: dotnet-version - run: | - TFM=$(grep -oPm1 '(?<=net)[0-9.]+' Directory.Build.props) - echo "version=${TFM}.x" >> "$GITHUB_OUTPUT" - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: ${{ steps.dotnet-version.outputs.version }} - - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: ~/.nuget/packages - key: ${{ runner.os }}-dotnet-${{ hashFiles('**/*.csproj', '**/Directory.Packages.props', '**/Directory.Build.props') }} - restore-keys: | - ${{ runner.os }}-dotnet- + + - name: Setup .NET environment + uses: ./.github/actions/dotnet-setup - name: Restore dependencies run: dotnet restore OpenSSH_GUI.slnx - - name: Build + - name: Build solution run: dotnet build OpenSSH_GUI.slnx --configuration Release --no-restore - - name: Run Tests - run: dotnet test OpenSSH_GUI.slnx --configuration Release --no-build --verbosity normal + - name: Run tests + run: dotnet test OpenSSH_GUI.slnx --configuration Release --no-build --verbosity normal \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index aa0b155..7627ab1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,7 @@ enable default https://github.com/frequency403/OpenSSH-GUI - 3.1.2 + 3.1.3 true diff --git a/openssh-gui-bin/PKGBUILD b/openssh-gui-bin/PKGBUILD index 8d67b0d..cd5acac 100644 --- a/openssh-gui-bin/PKGBUILD +++ b/openssh-gui-bin/PKGBUILD @@ -1,5 +1,5 @@ pkgname=openssh-gui-bin -pkgver=3.1.2 +pkgver=3.1.3 pkgrel=1 pkgdesc="A GUI for OpenSSH configuration and management (Binary version)" arch=('x86_64') diff --git a/openssh-gui-git/PKGBUILD b/openssh-gui-git/PKGBUILD index bb37a2d..4455ff1 100644 --- a/openssh-gui-git/PKGBUILD +++ b/openssh-gui-git/PKGBUILD @@ -2,7 +2,7 @@ pkgname=openssh-gui-git _pkgname=OpenSSH-GUI pkgver=2.2.1.r0.g845610b pkgrel=1 -pkgdesc="A GUI for OpenSSH configuration and management (GIT version, built from development branch)" +pkgdesc="A GUI for OpenSSH configuration and management (Sourcepackage)" arch=('x86_64') url="https://github.com/frequency403/OpenSSH-GUI" license=('MIT') diff --git a/update-version.sh b/update-version.sh deleted file mode 100644 index 6dc234c..0000000 --- a/update-version.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -PROPS="Directory.Build.props" -VERSION=$(grep -oP '(?<=)[^<]+' "${PROPS}") - -echo "→ Version: ${VERSION}" - -PKGBUILD_BIN="openssh-gui-bin/PKGBUILD" -sed -i "s/^pkgver=.*/pkgver=${VERSION}/" "${PKGBUILD_BIN}" -sed -i "s/^pkgrel=.*/pkgrel=1/" "${PKGBUILD_BIN}" -(cd openssh-gui-bin && updpkgsums) - -echo "✓ Done – ${PKGBUILD_BIN} → ${VERSION}" \ No newline at end of file