diff --git a/.github/actions/apply/action.yml b/.github/actions/apply/action.yml new file mode 100644 index 0000000..b445b4d --- /dev/null +++ b/.github/actions/apply/action.yml @@ -0,0 +1,78 @@ +name: PackageCascade apply +description: Download a pinned PackageCascade release and apply a prepared release plan locally + +inputs: + version: + description: PackageCascade release tag (for example, v1.2.3) + required: true + plan-path: + description: Path to a PackageCascade plan JSON file + required: true + output-directory: + description: Directory for generated package archives + required: true + repository: + description: Path to the Git repository (defaults to the working directory) + required: false + include-symbols: + description: Generate symbol packages (true or false; defaults to false) + required: false + default: 'false' + +outputs: + result-path: + description: Absolute path to the apply result JSON + value: ${{ steps.run.outputs.result-path }} + +runs: + using: composite + steps: + - id: run + shell: bash + env: + PACKAGECASCADE_VERSION: ${{ inputs.version }} + PACKAGECASCADE_PLAN_PATH: ${{ inputs.plan-path }} + PACKAGECASCADE_OUTPUT_DIRECTORY: ${{ inputs.output-directory }} + PACKAGECASCADE_REPOSITORY: ${{ inputs.repository }} + PACKAGECASCADE_INCLUDE_SYMBOLS: ${{ inputs.include-symbols }} + PACKAGECASCADE_RELEASE_URL: https://github.com/GoSimulate/PackageCascade/releases/download + run: | + set -euo pipefail + case "$RUNNER_OS/$RUNNER_ARCH" in + Linux/X64) asset=PackageCascade-linux-x64 ;; + Windows/X64) asset=PackageCascade-win-x64.exe ;; + macOS/X64) asset=PackageCascade-osx-x64 ;; + macOS/ARM64) asset=PackageCascade-osx-arm64 ;; + *) echo "No PackageCascade release asset for $RUNNER_OS/$RUNNER_ARCH" >&2; exit 1 ;; + esac + + temp_root="$RUNNER_TEMP" + output_file="$GITHUB_OUTPUT" + if [[ "$RUNNER_OS" == Windows ]]; then + temp_root="$(cygpath -u "$temp_root")" + output_file="$(cygpath -u "$output_file")" + fi + + download_dir="$(mktemp -d "$temp_root/packagecascade.XXXXXXXX")" + executable="$download_dir/$asset" + curl --fail --location --silent --show-error \ + "$PACKAGECASCADE_RELEASE_URL/$PACKAGECASCADE_VERSION/$asset" --output "$executable" + chmod +x "$executable" + + result_path="$download_dir/packagecascade-apply-result.json" + arguments=(apply --infile "$PACKAGECASCADE_PLAN_PATH" --output "$PACKAGECASCADE_OUTPUT_DIRECTORY") + if [[ -n "$PACKAGECASCADE_REPOSITORY" ]]; then + arguments+=(--repository "$PACKAGECASCADE_REPOSITORY") + fi + case "$PACKAGECASCADE_INCLUDE_SYMBOLS" in + true) arguments+=(--include-symbols) ;; + false) ;; + *) echo 'include-symbols must be true or false' >&2; exit 1 ;; + esac + "$executable" "${arguments[@]}" > "$result_path" + test -s "$result_path" + + if [[ "$RUNNER_OS" == Windows ]]; then + result_path="$(cygpath -w "$result_path")" + fi + printf 'result-path=%s\n' "$result_path" >> "$output_file" diff --git a/.github/actions/plan/action.yml b/.github/actions/plan/action.yml new file mode 100644 index 0000000..e9d749e --- /dev/null +++ b/.github/actions/plan/action.yml @@ -0,0 +1,71 @@ +name: PackageCascade plan +description: Download a pinned PackageCascade release and create a release plan + +inputs: + version: + description: PackageCascade release tag (for example, v1.2.3) + required: true + solution: + description: Path to the solution file + required: true + repository: + description: Path to the Git repository (defaults to the working directory) + required: false + configuration: + description: MSBuild configuration (defaults to Release) + required: false + +outputs: + plan-path: + description: Absolute path to the generated plan JSON + value: ${{ steps.run.outputs.plan-path }} + +runs: + using: composite + steps: + - id: run + shell: bash + env: + PACKAGECASCADE_VERSION: ${{ inputs.version }} + PACKAGECASCADE_SOLUTION: ${{ inputs.solution }} + PACKAGECASCADE_REPOSITORY: ${{ inputs.repository }} + PACKAGECASCADE_CONFIGURATION: ${{ inputs.configuration }} + PACKAGECASCADE_RELEASE_URL: https://github.com/GoSimulate/PackageCascade/releases/download + run: | + set -euo pipefail + case "$RUNNER_OS/$RUNNER_ARCH" in + Linux/X64) asset=PackageCascade-linux-x64 ;; + Windows/X64) asset=PackageCascade-win-x64.exe ;; + macOS/X64) asset=PackageCascade-osx-x64 ;; + macOS/ARM64) asset=PackageCascade-osx-arm64 ;; + *) echo "No PackageCascade release asset for $RUNNER_OS/$RUNNER_ARCH" >&2; exit 1 ;; + esac + + temp_root="$RUNNER_TEMP" + output_file="$GITHUB_OUTPUT" + if [[ "$RUNNER_OS" == Windows ]]; then + temp_root="$(cygpath -u "$temp_root")" + output_file="$(cygpath -u "$output_file")" + fi + + download_dir="$(mktemp -d "$temp_root/packagecascade.XXXXXXXX")" + executable="$download_dir/$asset" + curl --fail --location --silent --show-error \ + "$PACKAGECASCADE_RELEASE_URL/$PACKAGECASCADE_VERSION/$asset" --output "$executable" + chmod +x "$executable" + + plan_path="$download_dir/packagecascade-plan.json" + arguments=(plan "$PACKAGECASCADE_SOLUTION" --outfile "$plan_path") + if [[ -n "$PACKAGECASCADE_REPOSITORY" ]]; then + arguments+=(--repository "$PACKAGECASCADE_REPOSITORY") + fi + if [[ -n "$PACKAGECASCADE_CONFIGURATION" ]]; then + arguments+=(--configuration "$PACKAGECASCADE_CONFIGURATION") + fi + "$executable" "${arguments[@]}" + test -s "$plan_path" + + if [[ "$RUNNER_OS" == Windows ]]; then + plan_path="$(cygpath -w "$plan_path")" + fi + printf 'plan-path=%s\n' "$plan_path" >> "$output_file" diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index f8ae4b7..0076c81 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -5,6 +5,7 @@ on: paths: - 'src/**' - '!src/*.Tests.*/**' + - '.github/actions/**' - '.github/workflows/cd.yml' workflow_dispatch: permissions: @@ -64,16 +65,22 @@ jobs: dotnet pack src/PackageCascade.slnx -c Release --no-build --no-restore -o artifacts/packages -p:Version="$VERSION" -p:ContinuousIntegrationBuild=true -p:RepositoryUrl="$REPOSITORY_URL" - - name: Publish Linux CLI + - name: Publish CLI binaries run: | - dotnet publish src/PackageCascade.Cli/PackageCascade.Cli.csproj \ - -c Release -r linux-x64 --self-contained true -o artifacts/cli \ - -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true \ - -p:DebugType=embedded \ - -p:Version="$VERSION" -p:ContinuousIntegrationBuild=true -p:RepositoryUrl="$REPOSITORY_URL" - mv artifacts/cli/PackageCascade.Cli artifacts/cli/PackageCascade-linux-x64 - - name: Check Linux CLI - run: artifacts/cli/PackageCascade-linux-x64 --help + for rid in linux-x64 win-x64 osx-x64 osx-arm64; do + output="artifacts/cli/$rid" + dotnet publish src/PackageCascade.Cli/PackageCascade.Cli.csproj \ + -c Release -r "$rid" --self-contained true -o "$output" \ + -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true \ + -p:DebugType=embedded \ + -p:Version="$VERSION" -p:ContinuousIntegrationBuild=true -p:RepositoryUrl="$REPOSITORY_URL" + + if [ "$rid" = win-x64 ]; then + mv "$output/PackageCascade.Cli.exe" "$output/PackageCascade-$rid.exe" + else + mv "$output/PackageCascade.Cli" "$output/PackageCascade-$rid" + fi + done - name: Generate release notes id: notes uses: requarks/changelog-action@v1.10.3 @@ -112,7 +119,7 @@ jobs: tag: ${{ needs.version.outputs.next }} commit: ${{ github.sha }} body: ${{ steps.notes.outputs.changes }} - artifacts: artifacts/packages/*.nupkg,artifacts/packages/*.snupkg,artifacts/cli/PackageCascade-linux-x64 + artifacts: artifacts/packages/*.nupkg,artifacts/packages/*.snupkg,artifacts/cli/*/PackageCascade-* artifactErrorsFailBuild: true makeLatest: true - uses: actions/upload-artifact@v7.0.2 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88e7b75..4a1f9b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: run: | if [ "$GITHUB_EVENT_NAME" = workflow_dispatch ] || [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = 0000000000000000000000000000000000000000 ]; then echo 'run=true' >> "$GITHUB_OUTPUT" - elif [ -n "$(git diff --name-only "$BASE_SHA" HEAD -- src/ global.json .github/workflows/ci.yml)" ]; then + elif [ -n "$(git diff --name-only "$BASE_SHA" HEAD -- src/ global.json .github/actions/ .github/workflows/ci.yml .github/workflows/cd.yml)" ]; then echo 'run=true' >> "$GITHUB_OUTPUT" else echo 'run=false' >> "$GITHUB_OUTPUT" @@ -47,3 +47,36 @@ jobs: run: dotnet build src/PackageCascade.slnx -c Release - name: Test run: dotnet test --solution src/PackageCascade.slnx -c Release --no-build --no-restore + cli-platforms: + needs: select + if: needs.select.outputs.run_tests == 'true' + strategy: + fail-fast: false + matrix: + include: + - os: blacksmith-2vcpu-ubuntu-2404 + rid: linux-x64 + binary: PackageCascade.Cli + - os: windows-2025 + rid: win-x64 + binary: PackageCascade.Cli.exe + - os: macos-15-intel + rid: osx-x64 + binary: PackageCascade.Cli + - os: macos-15 + rid: osx-arm64 + binary: PackageCascade.Cli + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + steps: + - uses: actions/checkout@v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-dotnet@v6.0.0 + with: + global-json-file: global.json + - name: Publish CLI + run: dotnet publish src/PackageCascade.Cli/PackageCascade.Cli.csproj -c Release -r ${{ matrix.rid }} --self-contained true -o artifacts/cli -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:DebugType=embedded + - name: Run CLI help + shell: bash + run: ./artifacts/cli/${{ matrix.binary }} --help diff --git a/README.md b/README.md index d30cdea..85ca0af 100644 --- a/README.md +++ b/README.md @@ -19,30 +19,61 @@ Your pipeline owns tests, package publication, pushing tags, and any recovery or ### Run the CLI -The CLI currently runs from a build of this repository. Install the SDK specified in `global.json`, then build it: +[GitHub releases](https://github.com/GoSimulate/PackageCascade/releases) containing these actions include a self-contained CLI for Linux x64, Windows x64, macOS x64, and macOS arm64. Download the binary for your runner; the tool itself does not require a .NET runtime. Planning and applying still require the SDK used by the consumer projects. -```sh -dotnet build src/PackageCascade.Cli/PackageCascade.Cli.csproj -c Release -``` - -From the repository you want to release, point `cli` at that build. These examples use Bash: +From the repository you want to release, point `cli` at the downloaded binary. These examples use Bash: ```sh -cli=/absolute/path/to/PackageCascade/src/PackageCascade.Cli/bin/Release/net11.0/PackageCascade.Cli.dll +cli=/absolute/path/to/PackageCascade-linux-x64 +chmod +x "$cli" -dotnet "$cli" plan src/Example.slnx --outfile release-plan.json +"$cli" plan src/Example.slnx --outfile release-plan.json # Inspect release-plan.json before applying it. -dotnet "$cli" apply --infile release-plan.json --output artifacts/packages +"$cli" apply --infile release-plan.json --output artifacts/packages ``` For a single pipeline step, the plan can be streamed directly: ```sh set -o pipefail -dotnet "$cli" plan src/Example.slnx | - dotnet "$cli" apply --output artifacts/packages +"$cli" plan src/Example.slnx | + "$cli" apply --output artifacts/packages ``` +### Use the GitHub Actions + +The `plan` and `apply` composite actions download a specified PackageCascade release and expose paths to their JSON results. This example is a workflow in the repository being released. Replace `` with a published PackageCascade tag, and use that same tag in each `uses` reference and `version` input. Change the solution path to match your repository. + +```yaml +name: Prepare packages +on: workflow_dispatch +jobs: + prepare: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7.0.1 + with: + fetch-depth: 0 + - uses: actions/setup-dotnet@v6.0.0 + with: + global-json-file: global.json + - id: plan + uses: GoSimulate/PackageCascade/.github/actions/plan@ + with: + version: + solution: src/Example.slnx + - id: apply + uses: GoSimulate/PackageCascade/.github/actions/apply@ + with: + version: + plan-path: ${{ steps.plan.outputs.plan-path }} + output-directory: artifacts/packages +``` + +The example reads the SDK version from the consumer repository's `global.json`; configure SDK installation for your repository if it uses another method. Run your own tests before applying the plan. The apply action exposes `result-path` for subsequent publication or tag-push steps. Both actions accept an optional `repository` path; plan also accepts `configuration`, while apply accepts `include-symbols`. + +To build the CLI from source instead, install the SDK specified in this repository's `global.json` and run `dotnet build src/PackageCascade.Cli/PackageCascade.Cli.csproj -c Release`. + Both commands default to the current directory as the Git repository; use `--repository` to select another. Planning defaults to `Release`; `plan --configuration Debug` or a custom configuration records that choice for apply. Add `apply --include-symbols` to generate `.snupkg` files. Apply writes JSON containing the output package paths and created tag names to standard output. The SDK needed to build PackageCascade does not dictate the target frameworks of your packages. Apply uses the consumer solution's SDK selection and project settings. Versions are supplied during the build, without editing your project files; a reference to another package keeps that package's own version.