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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.sh text eol=lf
130 changes: 124 additions & 6 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@ name: CI

on:
push:
branches: ['**']
pull_request:
workflow_call:
inputs:
version:
required: true
type: string
commit:
required: true
type: string

permissions:
contents: read
Expand All @@ -11,44 +20,153 @@ jobs:
validate:
name: Validate (${{ matrix.os }})
runs-on: ${{ matrix.os }}
env:
RELEASE_VERSION: ${{ inputs.version || 'v0.0.0' }}
BINARY: ${{ matrix.binary }}
DOTNET_CLI_TELEMETRY_OPTOUT: '1'
DOTNET_NOLOGO: '1'
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
artifact: linux
binary: dscpkg
test_args: -race
- os: windows-latest
artifact: windows
binary: dscpkg.exe
test_args: ""
- os: macos-latest
artifact: macos
binary: dscpkg
test_args: -race

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ inputs.commit || github.sha }}
persist-credentials: false

- name: Set package directories
shell: pwsh
run: |
"DIST_DIR=$(Join-Path $env:RUNNER_TEMP 'dscpkg-dist')" >> $env:GITHUB_ENV
"FIXTURE_DIR=$(Join-Path $env:RUNNER_TEMP 'dscpkg-upgrade-fixtures')" >> $env:GITHUB_ENV
"DSCPKG_LOG_DIRECTORY=$(Join-Path $env:RUNNER_TEMP 'dscpkg-package-logs')" >> $env:GITHUB_ENV

- uses: actions/setup-go@v5
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version-file: go.mod
cache: true

- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
if: runner.os == 'Windows'
with:
dotnet-version: '8.0.x'

- name: Check formatting
if: runner.os == 'Linux'
shell: bash
run: gofmt -w $(git ls-files '*.go') && git diff --exit-code -- '*.go'
run: |
git ls-files -z '*.go' | xargs -0 -r gofmt -w
git diff --exit-code -- '*.go'

- name: Vet
run: go vet ./...

- name: Test
run: go test ${{ matrix.test_args }} ./...

- name: Test release safeguards
shell: pwsh
run: |
./packaging/test-version.ps1
./packaging/test-release.ps1

- name: Install Linux packaging and validation tools
if: runner.os == 'Linux'
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y rpm shellcheck
mkdir -p "$RUNNER_TEMP/package-tools"
GOBIN="$RUNNER_TEMP/package-tools" go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.47.0
GOBIN="$RUNNER_TEMP/package-tools" go install github.com/rhysd/actionlint/cmd/actionlint@v1.7.7
echo "$RUNNER_TEMP/package-tools" >> "$GITHUB_PATH"

- name: Lint workflows and packaging scripts
if: runner.os == 'Linux'
shell: bash
run: |
actionlint
shellcheck packaging/linux/*.sh

- name: Build
run: go build -trimpath -o ${{ matrix.binary }} ./cmd/dscpkg
shell: pwsh
env:
CGO_ENABLED: '0'
run: go build -trimpath -o $env:BINARY ./cmd/dscpkg

- uses: actions/upload-artifact@v4
- name: Build and inspect release assets
shell: pwsh
run: |
./packaging/build.ps1 -Version $env:RELEASE_VERSION -BinaryPath $env:BINARY -OutputDirectory $env:DIST_DIR
./packaging/verify.ps1 -Version $env:RELEASE_VERSION -BinaryPath $env:BINARY -Directory $env:DIST_DIR

- name: Build isolated upgrade fixtures
if: runner.os != 'macOS'
shell: pwsh
run: ./packaging/test-fixtures.ps1 -OutputDirectory $env:FIXTURE_DIR

- name: Test Windows installer lifecycle
if: runner.os == 'Windows'
shell: pwsh
run: |
./packaging/windows/test-install.ps1 -Disposable -Version $env:RELEASE_VERSION `
-Directory $env:DIST_DIR -BinaryPath $env:BINARY -FixtureDirectory $env:FIXTURE_DIR `
-LogDirectory $env:DSCPKG_LOG_DIRECTORY

- name: Test DEB and RPM installer lifecycles
if: runner.os == 'Linux'
shell: bash
run: |
mkdir -p "$DSCPKG_LOG_DIRECTORY"
sudo env GITHUB_ACTIONS="$GITHUB_ACTIONS" RUNNER_ENVIRONMENT="$RUNNER_ENVIRONMENT" \
bash packaging/linux/test-install.sh --disposable deb "$RELEASE_VERSION" \
"$DIST_DIR" "$PWD/dscpkg" "$FIXTURE_DIR" "$DSCPKG_LOG_DIRECTORY"
docker run --rm -e GITHUB_ACTIONS -e RUNNER_ENVIRONMENT \
-v "$PWD:/src:ro" -v "$DIST_DIR:/assets:ro" -v "$FIXTURE_DIR:/fixtures:ro" \
-v "$DSCPKG_LOG_DIRECTORY:/logs" fedora:44 \
bash -c 'dnf -y install diffutils && exec bash /src/packaging/linux/test-install.sh --disposable rpm "$@"' \
bash "$RELEASE_VERSION" /assets /src/dscpkg /fixtures /logs

- name: Recheck tested asset checksums
shell: pwsh
run: |
. ./packaging/common.ps1
$platform = Get-BinaryPlatform $env:BINARY
Assert-Checksums $env:DIST_DIR @(Get-AssetNames $env:RELEASE_VERSION $platform)

- name: Upload installer diagnostics
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: dscpkg-install-logs-${{ matrix.artifact }}
path: ${{ env.DSCPKG_LOG_DIRECTORY }}
overwrite: true

- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: dscpkg-${{ runner.os }}-${{ runner.arch }}
path: ${{ matrix.binary }}
if-no-files-found: error
if-no-files-found: error
overwrite: true

- name: Upload verified release payloads
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: dscpkg-release-${{ matrix.artifact }}
path: ${{ env.DIST_DIR }}/*
if-no-files-found: error
overwrite: true
76 changes: 76 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Release

on:
push:
tags: ['v*']

permissions:
contents: read

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
preflight:
runs-on: ubuntu-latest
outputs:
commit: ${{ steps.source.outputs.commit }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Validate release tag and source
id: source
shell: pwsh
env:
VERSION: ${{ github.ref_name }}
SOURCE_SHA: ${{ github.sha }}
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
$commit = ./packaging/release.ps1 -Operation Preflight -Version $env:VERSION `
-Commit $env:SOURCE_SHA -Repository $env:GH_REPO
"commit=$commit" >> $env:GITHUB_OUTPUT

build:
needs: preflight
uses: ./.github/workflows/ci.yaml
with:
version: ${{ github.ref_name }}
commit: ${{ needs.preflight.outputs.commit }}

draft:
needs: [preflight, build]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ needs.preflight.outputs.commit }}
persist-credentials: false
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: dscpkg-release-linux
path: dist/linux
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: dscpkg-release-windows
path: dist/windows
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: dscpkg-release-macos
path: dist/macos
- name: Verify tested assets and create an unpublished draft
shell: pwsh
env:
VERSION: ${{ github.ref_name }}
SOURCE_SHA: ${{ needs.preflight.outputs.commit }}
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
./packaging/release.ps1 -Operation Draft -Version $env:VERSION `
-Commit $env:SOURCE_SHA -Repository $env:GH_REPO -AssetDirectory ./dist
126 changes: 126 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,129 @@ The GitHub Actions workflow runs formatting, vetting, tests (with race detection
Linux and macOS), and builds on Linux, Windows, and macOS. It uploads one executable artifact
for each runner operating system. Go build caching is enabled through
`actions/setup-go`.

CI also builds and inspects release archives and native installers. Windows MSI,
Ubuntu DEB, and containerized Fedora RPM tests cover fresh installation,
reinstallation/repair, upgrade replacement using isolated fixtures, uninstall,
and preservation of downloaded resources and caches. Windows checks system PATH
registration/removal and rejects downgrades. Installer tests run only on
disposable GitHub-hosted runners, never on a developer workstation.

## Release downloads and installation

Releases include Linux AMD64 and Windows AMD64 archives, a macOS archive for the
native architecture of the macOS CI runner, Windows MSI, Linux DEB/RPM, and
`SHA256SUMS`. macOS filenames use Go's `darwin` platform name and the actual
`amd64` or `arm64` build architecture. Extract an archive and put its `bin`
directory on PATH, or use the appropriate native installer:

```sh
sudo apt install ./dscpkg_1.2.3_amd64.deb
# On an RPM-based distribution:
sudo dnf install ./dscpkg-1.2.3-1.x86_64.rpm
```

On Windows, run `dscpkg-v1.2.3-windows-amd64.msi` as administrator, or use:

```powershell
msiexec /i dscpkg-v1.2.3-windows-amd64.msi /qn /norestart
```

The MSI installs to `%ProgramFiles%\dscpkg` and adds that directory to the system
PATH; open a new terminal after installation. DEB/RPM install `/usr/bin/dscpkg`.
These are machine-wide CLI installations, not services. No DSC runtime is
bundled or required to install dscpkg.

Use Installed apps on Windows, `sudo apt remove dscpkg`, or `sudo dnf remove
dscpkg` to uninstall. Upgrades replace the executable; uninstall removes the
executable and its installer-managed PATH entry, but never deletes users'
downloaded resource packages, registries, or caches. Linux follows the native
package manager's version ordering; MSI blocks downgrades.

Packages are unsigned. Verify downloaded files against the release's
`SHA256SUMS` before installation. On Linux, use
`sha256sum --check --ignore-missing SHA256SUMS`; on macOS, compare
`shasum -a 256 <download>`; on Windows, compare
`Get-FileHash -Algorithm SHA256 <download>` with its manifest entry. Checksums
detect corruption but do not replace trusted download provenance or signing.

## Building release packages

Packaging requires PowerShell 7.2 or later, Git, Go, and native platform tools.
Windows additionally requires the .NET 8 SDK/runtime (or a compatible installed
toolchain); the project pins WiX SDK 7.0.0. Review the
[WiX license and fee requirements](https://docs.firegiant.com/wix/)
before using that toolchain; its build-time EULA setting is recorded in the
WiX project. This does not change dscpkg's MIT license.

Linux additionally requires Bash, tar, nFPM 2.47.0, `dpkg-deb`, `rpm`, and
`rpm2archive` (provided by Ubuntu's RPM tooling). Install nFPM as a separate build
tool, not a runtime module dependency:

```sh
go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.47.0
# Ensure Go's bin directory is on PATH. Go may download nFPM's required toolchain.
```

On each native platform, build one executable with CGO disabled and pass that
same binary to the packager. The output directory must be empty. For example,
in PowerShell on Windows:

```powershell
$env:CGO_ENABLED = '0'
go build -trimpath -o dscpkg.exe ./cmd/dscpkg
./packaging/build.ps1 -Version v1.2.3 -BinaryPath ./dscpkg.exe -OutputDirectory ./dist
./packaging/verify.ps1 -Version v1.2.3 -BinaryPath ./dscpkg.exe -Directory ./dist
```

On Linux or macOS, use `CGO_ENABLED=0 go build -trimpath -o dscpkg ./cmd/dscpkg`,
then invoke the same scripts with `pwsh -NoProfile -File`, `-BinaryPath ./dscpkg`,
and the other arguments above. Inspection extracts packages without installing
the product. Windows MSI binding/ICE validation requires a Windows environment
whose installer policy permits validation; do not disable validation to produce
a release.

`packaging/test-version.ps1` tests tag/version boundaries.
`packaging/test-release.ps1` tests release safeguards using an isolated Git
repository and mocked GitHub API, without creating real tags or releases.
CI uses `v0.0.0` for non-release package checks. Separate `v1.0.0`/`v1.0.1`
fixtures exercise upgrades even before the first release; they are never shipped.

## Creating a draft release

After the workflow changes are merged, push a stable `vX.Y.Z` tag pointing to a
reviewed commit already in `main`:

```sh
git fetch origin
git tag v1.2.3 <reviewed-commit>
git push origin refs/tags/v1.2.3
```

The [release workflow](.github/workflows/release.yaml) validates the tag and runs
reusable CI on its exact commit. Lightweight and annotated tags are supported.
Tags must have exactly three numeric components without leading zeroes; RC tags,
build metadata, and other prereleases are unsupported. MSI uses the direct
`X.Y.Z` version: major/minor must be at most 255, and patch at most 65535. These
limits apply to the entire release, not just its MSI.

Every platform's archive and installer contain the same executable bytes. After
all native checks pass, the final job downloads those exact tested assets from
the same workflow run, verifies the expected file sets and SHA-256 checksums,
and rechecks the remote tag. It creates a **draft normal release**, with generated
release notes and a combined `SHA256SUMS`, not a published release or prerelease.
Review its assets, source commit, and workflow results in **Releases**, then
publish manually. The workflow never creates or moves a tag.

An existing release or draft stops the run rather than being overwritten.
Authentication/API failures are errors, not evidence that a release is absent.
If creation partially fails, inspect the draft and uploaded assets; remove an
incomplete draft manually only after review before retrying the unchanged tag.
For source fixes, merge the fix and push a new version tag instead of moving one.

Re-run failed jobs while the original artifacts remain available. A rebuilt
platform must pass its complete package checks again; never skip them to make
a draft. If artifacts have expired, re-run the whole workflow. Installer failure
diagnostics are available as `dscpkg-install-logs-<platform>` artifacts. There is
no automatic publication, signing/notarization, macOS PKG, package repository,
SBOM, or attestation in this flow.
Loading
Loading