diff --git a/.github/AGENTS.md b/.github/AGENTS.md index b8dde0f5a..12e64e51e 100644 --- a/.github/AGENTS.md +++ b/.github/AGENTS.md @@ -11,7 +11,7 @@ - Keep trigger shapes intentional. This repo currently uses pull request triggers such as `opened`, `synchronize`, `reopened`, and `ready_for_review` where needed. - Preserve `persist-credentials: false` on checkout steps unless there is a concrete reason to change authentication behavior. - Reuse local composite actions in `.github/actions/` when they already capture setup shared across workflows. -- Treat release behavior as merge-driven: pushes to `master` and manual `workflow_dispatch` runs feed the current release flow documented in `.github/RELEASING.md`. +- Treat release behavior as split by stability: pushes to `master` update the rolling prerelease, and manual `workflow_dispatch` runs create protected stable releases as documented in `.github/RELEASING.md`. ## Verification diff --git a/.github/RELEASING.md b/.github/RELEASING.md index 1bd4aa384..6e5a4df75 100644 --- a/.github/RELEASING.md +++ b/.github/RELEASING.md @@ -7,132 +7,73 @@ Recommended versioning for the next release line: - Start the next stable release at `v0.10.0`. - Keep the binary's embedded version in source as a normal semver such as `0.10.0`. - Let merge-driven builds update a single rolling `prerelease` tag. -- Reserve semver tags like `v0.10.x` for stable releases. +- Reserve stable semver tags like `v0.10.0` for protected stable releases. -### 1. New Feature or Breaking‑Change Release (Minor/Major) +### Prerelease -1. **Merge & Verify** +1. Merge to `master`. +2. The `Prerelease` workflow runs tests, builds the seven supported binaries, + assembles `gomud-ALL-datafiles.zip` and `SHA256SUMS.txt`, generates + attestations, and updates the mutable `prerelease` GitHub prerelease. +3. The workflow may move the `prerelease` tag and clobber existing prerelease + assets. This is intentional so the rolling prerelease remains mutable. +4. The prerelease is marked as a prerelease and is not marked as `Latest`. -- Merge all feature or breaking‑change PRs into `master`. -- Ensure CI (tests, linter, codegen) all pass on `master`. +Pull requests do not publish release binaries. The generic `CI` workflow runs +the PR test gate; `master` release testing happens inside the `Prerelease` +workflow to avoid duplicate full race-test runs on merge. -1. **Determine Version Bump** +### Stable Release -- **Major** (`X.0.0`) when you make incompatible changes -- **Minor** (`0.Y.0`) when you add functionality in a backward compatible manner -- **Patch** (`0.0.Z`) when you make backward compatible bug fixes +1. Confirm the source version in `main.go` is correct. +2. Run the `Stable Release` workflow manually with a new stable semver + `release_tag`, such as `v0.10.0`. +3. The workflow fails before publishing if the requested tag or GitHub release + already exists. +4. The `publish` job uses the `stable-release` GitHub Environment. Configure + required reviewers on that environment in repository settings. +5. After environment approval, the workflow creates a draft release, uploads + assets, attaches release notes, then publishes the stable release. -1. **Merge to `master`** - - Merging to `master` triggers the `Release` workflow automatically. +Stable release workflow policy does not move tags, use `--clobber`, or replace +existing releases. Keep repository-wide immutable releases disabled so the +rolling `prerelease` release can remain mutable. - Or, for a manual test without merging: - - Run the `Release` workflow with `workflow_dispatch`. - - Optionally set `release_tag` if you want the run to upload assets to a - specific existing or new release tag instead of the default prerelease tag. - - Run `Release Latest Assets` when you want the Actions UI path that updates - the repo's current latest GitHub release tag without any manual input. +### Assets -2. **Monitor Release** - - GitHub Actions will: - - Run `go generate ./...` - - Build per-platform binaries with `main.version` set from `main.go` - - Replace the rolling `prerelease` tag and GitHub prerelease - - Archive `_datafiles` as `gomud-ALL-datafiles-prerelease.zip` - - Generate `gomud-prerelease-SHA256SUMS.txt` - - Leave the release unmarked as `Latest` +Prerelease and stable release assets are defined by +`.github/scripts/release-assets.sh`. Each release includes the supported +OS/architecture binaries, `gomud-ALL-datafiles.zip`, and `SHA256SUMS.txt`. -3. **Announce** - - After review, a repo owner can edit the release in GitHub and promote it to - `Latest`. - - Share the release link with the team or via configured notifications. +Release notes are generated by `.github/scripts/release-notes.sh` and include `Overview`, +`Downloads`, `Install From Source`, `Manual Binary Install`, `Verify Provenance`, +and `Changes`. The `Changes` section appends GitHub auto-generated notes from +`releases/generate-notes`. ---- +### Verification -### 2. Merge-Driven Prerelease Policy +After a release workflow succeeds: -1. **Pull requests do not publish release binaries** - - PRs should run normal CI only. +- Confirm the expected assets are attached. +- Confirm `SHA256SUMS.txt` verifies downloaded assets with `sha256sum -c`. +- Confirm binaries and `SHA256SUMS.txt` have artifact attestations. +- For stable releases, confirm the release tag points at the intended commit and + the release is no longer a draft. -2. **Merges to `master` do publish release binaries** - - A push to `master` runs the `Release` workflow and replaces the rolling - `prerelease`. - -3. **Manual test runs can also publish prereleases** - - `workflow_dispatch` can be used to refresh the rolling `prerelease` - without merging. - - If you provide `release_tag`, the run publishes to that tag instead. - - `Release Latest Assets` is the no-input manual workflow that resolves the - current latest GitHub release tag and publishes to it. - -4. **Rolling release naming** - - The release tag is always `prerelease`. - - The release notes record the commit SHA and publish time for the current build. - - Numbered releases such as `v0.10.0` remain the permanent download history. - ---- - -### 3. Manual Test Release Flow - -1. **Run the release workflow manually** - - Use `workflow_dispatch` when you want a test release - without merging to `master`. - - Leave `release_tag` blank to use the default manual prerelease naming. - - Set `release_tag` when you want the run to upload assets to a specific - existing or new release tag. - - Run `Release Latest Assets` when you want the no-input Actions UI path - that uploads assets to whichever tag GitHub currently considers the latest - release. - -2. **Verify the GitHub release** - - Confirm the workflow succeeds. - - Confirm the targeted release now points at the expected commit. - - Confirm the per-platform binaries are attached. - - Confirm the `_datafiles` zip asset is attached. - - Confirm the checksum manifest asset is attached. - - For default manual prerelease runs, confirm GitHub marks the release as a - prerelease and does not mark it as `Latest`. - -3. **Clean up if needed** - - No cleanup is normally required for the rolling `prerelease` path because - the next successful run replaces it. - - Tag-targeted manual runs use the tag you requested, so cleanup is only - needed if you created a temporary release tag for testing. - ---- - -### FAQ / Guidelines +### FAQ - **Does every merge to `master` trigger a release?** - Yes - every push to `master` runs the release workflow and publishes a prerelease. - That prerelease is the rolling `prerelease` entry, not a newly named release. - -- **Is auto-tagging enabled?** - Stable semver tags are not generated automatically. The merge-driven workflow - updates the rolling `prerelease` tag instead. - -- **Can I create a test release without merging to `master`?** - Yes - run the `Release` workflow manually with `workflow_dispatch`. That keeps PR - submissions clean while still allowing an on-demand refresh of `prerelease`. - Set `release_tag` when you want the run to publish assets to a specific tag. - Run `Release Latest Assets` when you want the no-input path that targets the - current latest release. - -- **Are workflow-created releases stable releases?** - No - the workflow creates prereleases. A repo owner must manually promote a release - to `Latest` in GitHub when it is approved. - -- **What assets should a release include?** - Each release should include separate per-platform binaries, a `_datafiles` zip, - and a checksum manifest so testers can download only what they need and still - verify the assets. + Yes. Every push to `master` runs the `Prerelease` workflow and updates the + rolling `prerelease`. -- **What tag format should we use going forward?** - Keep the source/binary version on the `0.10.x` line. Use `prerelease` for the - rolling master build and semver tags for stable releases. +- **Can the workflow publish a stable release from an existing tag?** + No. Stable releases must use a new semver tag. Existing tags and releases fail + the workflow. -- **When should I bump minor vs. patch?** - - **Minor** for new, backward‑compatible features. - - **Patch** for bug fixes or documentation tweaks. +- **Can stable assets be replaced?** + No. The stable workflow does not clobber assets. Publish a new patch release + tag instead. - **What about `go generate` directives?** - The workflow runs `go generate ./...` automatically before each build. + Release workflows run `go generate ./...` before tests and before each + cross-compiled build. diff --git a/.github/act/actrc b/.github/act/actrc new file mode 100644 index 000000000..9f92c62bf --- /dev/null +++ b/.github/act/actrc @@ -0,0 +1 @@ +# Repo-local act config placeholder. diff --git a/.github/act/push_tag.json b/.github/act/push_tag.json deleted file mode 100644 index 4001b8349..000000000 --- a/.github/act/push_tag.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "ref": "refs/tags/v0.0.0" -} diff --git a/.github/act/stable_release.json b/.github/act/stable_release.json new file mode 100644 index 000000000..510a66cca --- /dev/null +++ b/.github/act/stable_release.json @@ -0,0 +1,6 @@ +{ + "ref": "refs/heads/master", + "inputs": { + "release_tag": "v0.0.0" + } +} diff --git a/.github/actions/codegen-and-test/action.yml b/.github/actions/codegen-and-test/action.yml deleted file mode 100644 index ffdb92413..000000000 --- a/.github/actions/codegen-and-test/action.yml +++ /dev/null @@ -1,40 +0,0 @@ ---- -name: "Codegen and Test" -description: "Run go generate and go test" -runs: - using: "composite" - steps: - - run: go generate ./... - shell: bash - - run: go test -race ./... - shell: bash - - name: Cross-compile release targets - run: | - tmp_dir="$(mktemp -d)" - trap 'rm -rf "$tmp_dir"' EXIT - - build_target() { - local goos="$1" - local goarch="$2" - local goarm="$3" - local output="$4" - - echo "::group::Build $output ($goos/$goarch${goarm:+ GOARM=$goarm})" - if [ -n "$goarm" ]; then - env GOOS="$goos" GOARCH="$goarch" GOARM="$goarm" \ - go build -v -o "$tmp_dir/$output" . - else - env GOOS="$goos" GOARCH="$goarch" \ - go build -v -o "$tmp_dir/$output" . - fi - echo "::endgroup::" - } - - build_target linux amd64 "" gomud-linux_x64 - build_target linux arm64 "" gomud-linux_arm64 - build_target linux arm 7 gomud-linux_armv7 - build_target windows amd64 "" gomud-windows_x64.exe - build_target windows arm64 "" gomud-windows_arm64.exe - build_target darwin amd64 "" gomud-darwin_x64 - build_target darwin arm64 "" gomud-darwin_arm64 - shell: bash diff --git a/.github/actions/go-checks/action.yml b/.github/actions/go-checks/action.yml new file mode 100644 index 000000000..7f68824b6 --- /dev/null +++ b/.github/actions/go-checks/action.yml @@ -0,0 +1,12 @@ +--- +name: "Go Checks" +description: "Refresh generated code and run the race-enabled Go test suite" +runs: + using: "composite" + steps: + - name: Generate code + run: go generate ./... + shell: bash + - name: Run Go tests + run: go test -race ./... + shell: bash diff --git a/.github/actions/setup-go/action.yml b/.github/actions/setup-go/action.yml index be61fde78..1460b7e8a 100644 --- a/.github/actions/setup-go/action.yml +++ b/.github/actions/setup-go/action.yml @@ -1,6 +1,6 @@ --- name: "Setup Go" -description: "Setup Go using go.mod" +description: "Install the Go toolchain pinned by go.mod and enable module cache" runs: using: "composite" steps: diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e82ec7a2b..450723651 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -45,7 +45,7 @@ updates: - "*" - package-ecosystem: "docker" - directory: "/docker/provisioning" + directory: "/provisioning" schedule: interval: "monthly" time: "06:00" @@ -62,7 +62,7 @@ updates: - "patch" - package-ecosystem: "docker" - directory: "/docker/terminal" + directory: "/provisioning/terminal" schedule: interval: "monthly" time: "06:00" diff --git a/.github/scripts/assemble-release-assets.sh b/.github/scripts/assemble-release-assets.sh new file mode 100755 index 000000000..b40094dc0 --- /dev/null +++ b/.github/scripts/assemble-release-assets.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd -- "${script_dir}/../.." && pwd)" +bin_dir="${RELEASE_BIN_DIR:-bin}" +datafiles_archive="${DATAFILES_ARCHIVE:-gomud-ALL-datafiles.zip}" +checksums_file="${CHECKSUMS_FILE:-SHA256SUMS.txt}" + +cd "$repo_root" + +mapfile -t checksum_assets < <( + DATAFILES_ARCHIVE="$datafiles_archive" \ + CHECKSUMS_FILE="$checksums_file" \ + "${script_dir}/release-assets.sh" checksum-names +) + +zip -qr "${bin_dir}/${datafiles_archive}" _datafiles + +cd "$bin_dir" +sha256sum "${checksum_assets[@]}" >"$checksums_file" diff --git a/.github/scripts/build-release-binaries.sh b/.github/scripts/build-release-binaries.sh new file mode 100755 index 000000000..6690f5283 --- /dev/null +++ b/.github/scripts/build-release-binaries.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd -- "${script_dir}/../.." && pwd)" +dist_dir="${RELEASE_DIST_DIR:-dist}" +binary_version="${BINARY_VERSION:-}" + +cd "$repo_root" +mkdir -p "$dist_dir" + +while IFS='|' read -r _label goos goarch goarm asset; do + target="${goos}/${goarch}" + if [ -n "$goarm" ]; then + target="${target} GOARM=${goarm}" + fi + + build_args=() + if [ -n "$binary_version" ]; then + build_args+=(-ldflags "-X main.version=${binary_version}") + fi + + echo "::group::Build ${asset} (${target})" + if [ -n "$goarm" ]; then + env GOOS="$goos" GOARCH="$goarch" GOARM="$goarm" \ + go build "${build_args[@]}" -o "${dist_dir}/${asset}" . + else + env GOOS="$goos" GOARCH="$goarch" \ + go build "${build_args[@]}" -o "${dist_dir}/${asset}" . + fi + echo "::endgroup::" +done < <("${script_dir}/release-assets.sh" targets) diff --git a/.github/scripts/ci-local-act.sh b/.github/scripts/ci-local-act.sh new file mode 100755 index 000000000..e113150c3 --- /dev/null +++ b/.github/scripts/ci-local-act.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd -- "${script_dir}/../.." && pwd)" + +ACT_FLAGS="${ACT_FLAGS:---pull=false -P ubuntu-24.04=catthehacker/ubuntu:act-latest}" +ACT_DRYRUN_SECRETS="${ACT_DRYRUN_SECRETS:--s DISCORD_WEBHOOK_URL=https://example.invalid/webhook}" +XDG_CONFIG_HOME="${ACT_CONFIG_HOME:-${repo_root}/.github}" +export XDG_CONFIG_HOME + +mkdir -p "${XDG_CONFIG_HOME}/act" +touch "${XDG_CONFIG_HOME}/act/actrc" + +run_act() { + local event="$1" + local event_file="$2" + local workflow="$3" + shift 3 + + act ${ACT_FLAGS:-} --dryrun "$event" "$@" \ + -e "$event_file" \ + -W "$workflow" +} + +# CI combines the old lint and PR test workflows. Dry-run both event shapes +# because pull requests cancel superseded runs while pushes to master do not. +run_act push .github/act/push_master.json .github/workflows/ci.yml +run_act pull_request .github/act/pull_request.json .github/workflows/ci.yml +run_act pull_request .github/act/pull_request.json \ + .github/workflows/discord-notify.yml ${ACT_DRYRUN_SECRETS:-} +run_act push .github/act/push_master.json .github/workflows/prerelease.yml +run_act workflow_dispatch .github/act/stable_release.json \ + .github/workflows/stable-release.yml +run_act push .github/act/push_master.json \ + .github/workflows/docker-package.yml ${ACT_DRYRUN_SECRETS:-} +run_act pull_request .github/act/pull_request.json \ + .github/workflows/docker-package.yml ${ACT_DRYRUN_SECRETS:-} diff --git a/.github/scripts/release-assets.sh b/.github/scripts/release-assets.sh new file mode 100755 index 000000000..9e81e5763 --- /dev/null +++ b/.github/scripts/release-assets.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +set -euo pipefail + +datafiles_archive="${DATAFILES_ARCHIVE:-gomud-ALL-datafiles.zip}" +checksums_file="${CHECKSUMS_FILE:-SHA256SUMS.txt}" + +release_targets() { + cat <<'EOF' +Linux amd64|linux|amd64||gomud-linux_x64 +Linux arm64|linux|arm64||gomud-linux_arm64 +Linux arm/v7|linux|arm|7|gomud-linux_armv7 +Windows amd64|windows|amd64||gomud-windows_x64.exe +Windows arm64|windows|arm64||gomud-windows_arm64.exe +macOS amd64|darwin|amd64||gomud-darwin_x64 +macOS arm64|darwin|arm64||gomud-darwin_arm64 +EOF +} + +binary_names() { + local label goos goarch goarm asset + + while IFS='|' read -r label goos goarch goarm asset; do + printf '%s\n' "$asset" + done < <(release_targets) +} + +binary_paths() { + local asset + + while IFS= read -r asset; do + printf 'bin/%s\n' "$asset" + done < <(binary_names) +} + +checksum_names() { + binary_names + printf '%s\n' "$datafiles_archive" +} + +upload_paths() { + binary_paths + printf 'bin/%s\n' "$datafiles_archive" + printf 'bin/%s\n' "$checksums_file" +} + +attestation_paths() { + binary_paths + printf 'bin/%s\n' "$checksums_file" +} + +downloads_markdown() { + local label goos goarch goarm asset + + while IFS='|' read -r label goos goarch goarm asset; do + printf -- '- %s: `%s`\n' "$label" "$asset" + done < <(release_targets) + printf -- '- Datafiles: `%s`\n' "$datafiles_archive" + printf -- '- Checksums: `%s`\n' "$checksums_file" +} + +case "${1:-}" in +targets) + release_targets + ;; +binary-names) + binary_names + ;; +binary-paths) + binary_paths + ;; +checksum-names) + checksum_names + ;; +upload-paths) + upload_paths + ;; +attestation-paths) + attestation_paths + ;; +downloads-markdown) + downloads_markdown + ;; +*) + printf 'Usage: %s COMMAND\n' "$0" >&2 + printf 'Commands: targets, binary-names, binary-paths, ' >&2 + printf 'checksum-names, upload-paths, attestation-paths, ' >&2 + printf 'downloads-markdown\n' >&2 + exit 2 + ;; +esac diff --git a/.github/scripts/release-notes.sh b/.github/scripts/release-notes.sh new file mode 100755 index 000000000..d5cfc789e --- /dev/null +++ b/.github/scripts/release-notes.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +set -euo pipefail + +notes_file="${RELEASE_NOTES_FILE:-release-notes.md}" +generated_notes_file="${GENERATED_NOTES_FILE:-generated-release-notes.md}" +release_kind="${RELEASE_KIND:-}" +release_tag="${RELEASE_TAG:-}" +binary_version="${BINARY_VERSION:-}" +repository="${GITHUB_REPOSITORY:-}" +commit_sha="${GITHUB_SHA:-}" +ref_name="${GITHUB_REF_NAME:-}" +datafiles_archive="${DATAFILES_ARCHIVE:-gomud-ALL-datafiles.zip}" +checksums_file="${CHECKSUMS_FILE:-SHA256SUMS.txt}" +downloads="$( + DATAFILES_ARCHIVE="$datafiles_archive" \ + CHECKSUMS_FILE="$checksums_file" \ + .github/scripts/release-assets.sh downloads-markdown +)" + +require_env() { + local name="$1" + local value="$2" + + if [ -z "$value" ]; then + printf '%s is required\n' "$name" >&2 + exit 1 + fi +} + +require_env RELEASE_KIND "$release_kind" +require_env RELEASE_TAG "$release_tag" +require_env BINARY_VERSION "$binary_version" +require_env GITHUB_REPOSITORY "$repository" +require_env GITHUB_SHA "$commit_sha" + +case "$release_kind" in +prerelease | stable) + ;; +*) + printf 'RELEASE_KIND must be prerelease or stable\n' >&2 + exit 1 + ;; +esac + +previous_tag="${PREVIOUS_TAG_NAME:-}" +if [ -z "$previous_tag" ] && [ "${RELEASE_NOTES_SKIP_GH:-}" != "true" ]; then + previous_tag="$( + gh api "repos/${repository}/releases/latest" \ + --jq '.tag_name' \ + 2>/dev/null || true + )" +fi + +if [ "${RELEASE_NOTES_SKIP_GH:-}" = "true" ]; then + printf 'Generated release notes skipped for local dry run.\n' \ + >"$generated_notes_file" +else + notes_tag="$release_tag" + if [ "$release_kind" = "prerelease" ]; then + # GitHub ignores target_commitish when tag_name already exists. + notes_tag="${release_tag}-notes-${commit_sha}" + fi + + generate_notes_args=( + -f "tag_name=${notes_tag}" + -f "target_commitish=${commit_sha}" + ) + if [ -n "$previous_tag" ] && [ "$previous_tag" != "$release_tag" ]; then + generate_notes_args+=(-f "previous_tag_name=${previous_tag}") + fi + + gh api \ + -X POST \ + "repos/${repository}/releases/generate-notes" \ + "${generate_notes_args[@]}" \ + --jq '.body' \ + >"$generated_notes_file" +fi + +published_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" +if [ -n "$previous_tag" ] && [ "$previous_tag" != "$release_tag" ]; then + changes_since="Changes since: \`${previous_tag}\`" +else + changes_since="Changes since: initial release history" +fi + +if [ "$release_kind" = "prerelease" ]; then + overview="Rolling prerelease build from \`${ref_name:-master}\`." + summary="This mutable prerelease is replaced on each successful merge to \`master\`." +else + overview="Stable release \`${release_tag}\`." + summary="This stable release is immutable. Tags and assets are not replaced by workflow policy." +fi + +cat >"$notes_file" < --repo ${repository}\`. + +## Changes + +EOF + +cat "$generated_notes_file" >>"$notes_file" diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml deleted file mode 100644 index c31c24b18..000000000 --- a/.github/workflows/build-and-release.yml +++ /dev/null @@ -1,345 +0,0 @@ ---- -name: Release - -"on": - push: - branches: - - master - workflow_dispatch: - inputs: - release_tag: - description: Existing/new release tag, or latest, to publish assets to - required: false - type: string - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: false - -permissions: - contents: read - -jobs: - prep: - runs-on: ubuntu-24.04 - timeout-minutes: 5 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - outputs: - binary_version: ${{ steps.meta.outputs.binary_version }} - release_tag: ${{ steps.meta.outputs.release_tag }} - release_title: ${{ steps.meta.outputs.release_title }} - release_notes_intro: ${{ steps.meta.outputs.release_notes_intro }} - release_notes_summary: ${{ steps.meta.outputs.release_notes_summary }} - datafiles_archive: ${{ steps.meta.outputs.datafiles_archive }} - checksums_file: ${{ steps.meta.outputs.checksums_file }} - steps: - # actions/checkout v6.0.2 - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - persist-credentials: false - - - name: Compute release metadata - id: meta - env: - REQUESTED_RELEASE_TAG: ${{ github.event.inputs.release_tag || '' }} - run: | - short_sha="$(printf '%s' "$GITHUB_SHA" | cut -c1-7)" - timestamp="$(date -u +'%Y%m%d%H%M%S')" - requested_release_tag="$REQUESTED_RELEASE_TAG" - binary_version="$( - awk -F'"' '/^const VERSION = "/ { print $2; exit }' main.go - )" - if [ -z "$binary_version" ]; then - echo "Could not determine binary version from main.go" >&2 - exit 1 - fi - - if [ -n "$requested_release_tag" ]; then - if [ "$requested_release_tag" = "latest" ]; then - if [ "$GITHUB_REF" != "refs/heads/master" ]; then - echo \ - "release_tag=latest may only be used when " \ - "workflow_dispatch runs from master" \ - >&2 - exit 1 - fi - release_tag="$( - gh api "repos/${GITHUB_REPOSITORY}/releases/latest" \ - --jq '.tag_name' - )" - if [ -z "$release_tag" ]; then - echo "Could not determine latest release tag" >&2 - exit 1 - fi - release_title="$release_tag" - release_notes_intro="Manual release build for current latest \ - release \`${release_tag}\` from \`${GITHUB_REF_NAME}\`." - else - case "$requested_release_tag" in - *[!A-Za-z0-9._-]*) - echo \ - "release_tag may contain only letters, digits, ., _, and \ - -" \ - >&2 - exit 1 - ;; - esac - - release_tag="$requested_release_tag" - release_title="$requested_release_tag" - release_notes_intro="Manual release build for \ - \`${requested_release_tag}\` from \`${GITHUB_REF_NAME}\`." - fi - release_notes_summary="This run publishes assets to the \ - requested release tag." - elif [ "$GITHUB_REF" = "refs/heads/master" ]; then - release_tag="prerelease" - release_title="prerelease" - release_notes_intro="Rolling prerelease build from \`master\`." - release_notes_summary="This release is replaced on each successful \ - merge to \`master\`." - else - release_tag="pre-${timestamp}-${short_sha}" - release_title="$release_tag" - release_notes_intro="Manual prerelease build from \ - \`${GITHUB_REF_NAME}\`." - release_notes_summary="This is an ad hoc prerelease for the \ - selected ref and does not replace the rolling \`master\` prerelease." - fi - - { - echo "binary_version=${binary_version}" - echo "release_tag=${release_tag}" - echo "release_title=${release_title}" - echo "release_notes_intro=${release_notes_intro}" - echo "release_notes_summary=${release_notes_summary}" - echo "datafiles_archive=gomud-ALL-datafiles.zip" - echo "checksums_file=SHA256SUMS.txt" - } >> "$GITHUB_OUTPUT" - - build: - runs-on: ubuntu-24.04 - timeout-minutes: 30 - needs: prep - env: - BINARY_VERSION: ${{ needs.prep.outputs.binary_version }} - steps: - # actions/checkout v6.0.2 - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - persist-credentials: false - - uses: ./.github/actions/setup-go - - - name: Generate code - run: go generate ./... - - - name: Run tests - run: go test -race ./... - - - name: Create bin directory - run: mkdir -p bin/ - - - name: Copy _datafiles to bin/ - run: cp -r _datafiles bin/ - - - name: Build windows amd64 - run: >- - env GOOS=windows GOARCH=amd64 go build -v - -ldflags "-X main.version=${BINARY_VERSION}" - -o bin/gomud-windows_x64.exe . - - - name: Build windows arm64 - run: >- - env GOOS=windows GOARCH=arm64 go build -v - -ldflags "-X main.version=${BINARY_VERSION}" - -o bin/gomud-windows_arm64.exe . - - - name: Build darwin/arm64 - run: >- - env GOOS=darwin GOARCH=arm64 go build -v - -ldflags "-X main.version=${BINARY_VERSION}" - -o bin/gomud-darwin_arm64 . - - - name: Build darwin/amd64 - run: >- - env GOOS=darwin GOARCH=amd64 go build -v - -ldflags "-X main.version=${BINARY_VERSION}" - -o bin/gomud-darwin_x64 . - - - name: Build linux/amd64 - run: >- - env GOOS=linux GOARCH=amd64 go build -v - -ldflags "-X main.version=${BINARY_VERSION}" - -o bin/gomud-linux_x64 . - - - name: Build linux/arm64 - run: >- - env GOOS=linux GOARCH=arm64 go build -v - -ldflags "-X main.version=${BINARY_VERSION}" - -o bin/gomud-linux_arm64 . - - - name: Build linux/armv7 - run: >- - env GOOS=linux GOARCH=arm GOARM=7 go build -v - -ldflags "-X main.version=${BINARY_VERSION}" - -o bin/gomud-linux_armv7 . - - - name: Upload bin - # actions/upload-artifact v7.0.1 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a - with: - name: bin-artifact - path: bin/ - - release: - runs-on: ubuntu-24.04 - timeout-minutes: 30 - permissions: - contents: write - needs: - - prep - - build - env: - RELEASE_TAG: ${{ needs.prep.outputs.release_tag }} - RELEASE_TITLE: ${{ needs.prep.outputs.release_title }} - BINARY_VERSION: ${{ needs.prep.outputs.binary_version }} - RELEASE_NOTES_INTRO: ${{ needs.prep.outputs.release_notes_intro }} - RELEASE_NOTES_SUMMARY: ${{ needs.prep.outputs.release_notes_summary }} - DATAFILES_ARCHIVE: ${{ needs.prep.outputs.datafiles_archive }} - CHECKSUMS_FILE: ${{ needs.prep.outputs.checksums_file }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REQUESTED_RELEASE_TAG: ${{ github.event.inputs.release_tag || '' }} - steps: - # actions/checkout v6.0.2 - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - persist-credentials: false - - - name: Download builds - # actions/download-artifact v8.0.1 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c - with: - name: bin-artifact - path: bin/ - - - name: Archive datafiles - run: >- - zip -r - "bin/${DATAFILES_ARCHIVE}" - bin/_datafiles - - - name: Generate release checksums - run: | - cd bin - sha256sum \ - gomud-windows_x64.exe \ - gomud-windows_arm64.exe \ - gomud-darwin_arm64 \ - gomud-darwin_x64 \ - gomud-linux_x64 \ - gomud-linux_arm64 \ - gomud-linux_armv7 \ - "${DATAFILES_ARCHIVE}" \ - > "${CHECKSUMS_FILE}" - - - name: Write release notes - run: | - latest_release_tag="$( - gh api "repos/${GITHUB_REPOSITORY}/releases/latest" \ - --jq '.tag_name' \ - 2>/dev/null || true - )" - generate_notes_args=( - -f "tag_name=release-notes-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" - -f "target_commitish=${GITHUB_SHA}" - ) - if [ -n "$latest_release_tag" ]; then - generate_notes_args+=(-f "previous_tag_name=${latest_release_tag}") - changes_since_line="- Changes since: \`${latest_release_tag}\`" - else - changes_since_line="- Changes since: initial release history" - fi - generated_release_notes="$( - gh api \ - -X POST \ - "repos/${GITHUB_REPOSITORY}/releases/generate-notes" \ - "${generate_notes_args[@]}" \ - --jq '.body' - )" - published_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - cat > release-notes.md </dev/null 2>&1; then - gh api \ - -X PATCH \ - "repos/${GITHUB_REPOSITORY}/git/refs/tags/${RELEASE_TAG}" \ - -f "sha=${GITHUB_SHA}" \ - -F force=true - gh release upload "$RELEASE_TAG" "${assets[@]}" --clobber - gh release edit "$RELEASE_TAG" \ - --title "$RELEASE_TITLE" \ - --target "$GITHUB_SHA" \ - --notes-file release-notes.md - else - if gh api \ - "repos/${GITHUB_REPOSITORY}/git/ref/tags/${RELEASE_TAG}" \ - >/dev/null 2>&1; then - gh api \ - -X PATCH \ - "repos/${GITHUB_REPOSITORY}/git/refs/tags/${RELEASE_TAG}" \ - -f "sha=${GITHUB_SHA}" \ - -F force=true - fi - gh release create "$RELEASE_TAG" \ - "${assets[@]}" \ - --title "$RELEASE_TITLE" \ - --target "$GITHUB_SHA" \ - --notes-file release-notes.md - fi - else - if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then - gh release upload "$RELEASE_TAG" "${assets[@]}" --clobber - gh release edit "$RELEASE_TAG" \ - --title "$RELEASE_TITLE" \ - --target "$GITHUB_SHA" \ - --prerelease \ - --latest=false \ - --notes-file release-notes.md - else - gh release create "$RELEASE_TAG" \ - "${assets[@]}" \ - --title "$RELEASE_TITLE" \ - --target "$GITHUB_SHA" \ - --prerelease \ - --latest=false \ - --notes-file release-notes.md - fi - fi diff --git a/.github/workflows/lint.yml b/.github/workflows/ci.yml similarity index 66% rename from .github/workflows/lint.yml rename to .github/workflows/ci.yml index c3732e58e..0da952dd8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,12 @@ --- -name: Lint +name: CI + +# ci.yml performs the Continuous Integration (CI) workflow and performs the +# following actions: +# - Go format/vet +# - conditional JS lint +# - Go race tests +# - Release cross-compile check "on": push: @@ -12,6 +19,7 @@ name: Lint - reopened - ready_for_review +# Superseded CI runs cancel previous runs concurrency: group: >- ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -22,12 +30,15 @@ permissions: jobs: detect-changes: + name: Detect JavaScript Changes runs-on: ubuntu-24.04 timeout-minutes: 5 outputs: js_lint: >- ${{ steps.filter.outputs.js_lint }} steps: + # Full history is needed so push and pull request events can diff against + # the actual base commit instead of whatever shallow checkout provides. # actions/checkout v6.0.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: @@ -59,6 +70,7 @@ jobs: fi go-lint: + name: Go Format And Vet runs-on: ubuntu-24.04 timeout-minutes: 20 steps: @@ -73,9 +85,11 @@ jobs: run: make validate js-lint: + name: JavaScript Lint runs-on: ubuntu-24.04 timeout-minutes: 10 needs: detect-changes + # The web client JavaScript is linted only when related files change. if: >- needs.detect-changes.outputs.js_lint == 'true' steps: @@ -92,3 +106,25 @@ jobs: - name: Run JavaScript lint checks run: make js-lint + + test: + name: Go Tests And Release Cross-Compile + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + # actions/checkout v6.0.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + persist-credentials: false + + - uses: ./.github/actions/setup-go + + - uses: ./.github/actions/go-checks + + - name: Cross-compile release targets + if: >- + github.event_name == 'push' && + github.ref == 'refs/heads/master' + env: + RELEASE_DIST_DIR: ${{ runner.temp }}/release-cross-compile + run: .github/scripts/build-release-binaries.sh diff --git a/.github/workflows/discord-notify.yml b/.github/workflows/discord-notify.yml index 6b597669f..293d185c9 100644 --- a/.github/workflows/discord-notify.yml +++ b/.github/workflows/discord-notify.yml @@ -12,6 +12,8 @@ permissions: jobs: notify-discord: + # Draft pull requests wait until ready_for_review so Discord only receives + # notifications for PRs that are ready for broader attention. if: ${{ !github.event.pull_request.draft }} runs-on: ubuntu-24.04 timeout-minutes: 5 @@ -25,6 +27,7 @@ jobs: run: | webhook_url='${{ secrets.DISCORD_WEBHOOK_URL }}' if [ -z "$webhook_url" ]; then + # Local act dry runs can pass this as an environment variable. webhook_url="${DISCORD_WEBHOOK_URL:-}" fi echo "url=$webhook_url" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/docker-package.yml b/.github/workflows/docker-package.yml index be4285315..ba47af57c 100644 --- a/.github/workflows/docker-package.yml +++ b/.github/workflows/docker-package.yml @@ -2,6 +2,7 @@ name: Docker Image "on": + # Manual dispatch lets maintainers rebuild the image without changing source. workflow_dispatch: push: branches: @@ -35,6 +36,7 @@ jobs: - name: Detect Go version id: go-version + # The Dockerfile build arg follows go.mod through the Makefile helper. run: echo "version=$(make go-version)" >> "$GITHUB_OUTPUT" - name: Set up Docker Buildx @@ -69,6 +71,8 @@ jobs: uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f with: context: . + # Pull requests build and cache the image, but only trusted events + # publish to GitHub Packages. push: ${{ github.event_name != 'pull_request' }} file: ./provisioning/Dockerfile build-args: | diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml new file mode 100644 index 000000000..412adfe9e --- /dev/null +++ b/.github/workflows/prerelease.yml @@ -0,0 +1,193 @@ +--- +name: Prerelease + +"on": + push: + branches: + - master + +# Prerelease is intentionally mutable: each successful merge to master replaces +# the existing prerelease assets and moves the prerelease tag forward. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: read + +env: + DATAFILES_ARCHIVE: gomud-ALL-datafiles.zip + CHECKSUMS_FILE: SHA256SUMS.txt + +jobs: + metadata: + name: Read Binary Version + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: read + outputs: + binary_version: ${{ steps.meta.outputs.binary_version }} + steps: + # actions/checkout v6.0.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + persist-credentials: false + + - name: Compute release metadata + id: meta + run: | + set -euo pipefail + + binary_version="$( + awk -F'"' '/^const VERSION = "/ { print $2; exit }' main.go + )" + if [ -z "$binary_version" ]; then + echo "Could not determine binary version from main.go" >&2 + exit 1 + fi + + echo "binary_version=${binary_version}" >> "$GITHUB_OUTPUT" + + test: + name: Validate Release Source + runs-on: ubuntu-24.04 + timeout-minutes: 30 + permissions: + contents: read + steps: + # actions/checkout v6.0.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + persist-credentials: false + + - uses: ./.github/actions/setup-go + + - uses: ./.github/actions/go-checks + + build: + name: Build Release Binaries + runs-on: ubuntu-24.04 + timeout-minutes: 20 + needs: + - metadata + - test + permissions: + contents: read + env: + BINARY_VERSION: ${{ needs.metadata.outputs.binary_version }} + steps: + # actions/checkout v6.0.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + persist-credentials: false + + - uses: ./.github/actions/setup-go + + - name: Generate code + run: go generate ./... + + - name: Build release binaries + run: .github/scripts/build-release-binaries.sh + + - name: Upload release binaries + # actions/uploadf-artifact v7.0.1 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: release-binaries + path: dist/* + if-no-files-found: error + + publish: + name: Publish Rolling Prerelease + runs-on: ubuntu-24.04 + timeout-minutes: 30 + needs: + - metadata + - build + permissions: + contents: write + id-token: write + attestations: write + env: + RELEASE_TAG: prerelease + RELEASE_TITLE: prerelease + BINARY_VERSION: ${{ needs.metadata.outputs.binary_version }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + # actions/checkout v6.0.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + persist-credentials: false + + - name: Download release binaries + # actions/download-artifact v8.0.1 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: release-binaries + path: bin/ + + - name: Assemble release assets + run: .github/scripts/assemble-release-assets.sh + + - name: Write release notes + env: + RELEASE_KIND: prerelease + RELEASE_NOTES_FILE: release-notes.md + run: .github/scripts/release-notes.sh + + - name: Resolve attestation paths + id: release-assets + # Attest binaries and checksums. The datafiles zip is covered by the + # checksum file because it is assembled inside this workflow. + run: | + { + echo 'attestation_paths<> "$GITHUB_OUTPUT" + + - name: Attest release assets + # actions/attest-build-provenance v4.1.0 + # yamllint disable-line rule:line-length + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 + with: + subject-path: ${{ steps.release-assets.outputs.attestation_paths }} + + - name: Publish rolling prerelease + run: | + set -euo pipefail + + mapfile -t assets < <( + .github/scripts/release-assets.sh upload-paths + ) + + # Move the lightweight prerelease tag when it already exists. If it + # does not exist, gh release create will create it at this commit. + if gh api \ + "repos/${GITHUB_REPOSITORY}/git/ref/tags/${RELEASE_TAG}" \ + >/dev/null 2>&1; then + gh api \ + -X PATCH \ + "repos/${GITHUB_REPOSITORY}/git/refs/tags/${RELEASE_TAG}" \ + -f "sha=${GITHUB_SHA}" \ + -F force=true + fi + + if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then + gh release upload "$RELEASE_TAG" "${assets[@]}" --clobber + gh release edit "$RELEASE_TAG" \ + --title "$RELEASE_TITLE" \ + --target "$GITHUB_SHA" \ + --prerelease \ + --latest=false \ + --notes-file release-notes.md + else + gh release create "$RELEASE_TAG" \ + "${assets[@]}" \ + --title "$RELEASE_TITLE" \ + --target "$GITHUB_SHA" \ + --prerelease \ + --latest=false \ + --notes-file release-notes.md + fi diff --git a/.github/workflows/release-latest-assets.yml b/.github/workflows/release-latest-assets.yml deleted file mode 100644 index 8e6a9afb4..000000000 --- a/.github/workflows/release-latest-assets.yml +++ /dev/null @@ -1,265 +0,0 @@ ---- -name: Release Latest Assets - -"on": - workflow_dispatch: - -concurrency: - group: Release-${{ github.ref }} - cancel-in-progress: false - -permissions: - contents: read - -jobs: - prep: - runs-on: ubuntu-24.04 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - outputs: - binary_version: ${{ steps.meta.outputs.binary_version }} - release_tag: ${{ steps.meta.outputs.release_tag }} - release_title: ${{ steps.meta.outputs.release_title }} - release_notes_intro: ${{ steps.meta.outputs.release_notes_intro }} - release_notes_summary: ${{ steps.meta.outputs.release_notes_summary }} - datafiles_archive: ${{ steps.meta.outputs.datafiles_archive }} - checksums_file: ${{ steps.meta.outputs.checksums_file }} - steps: - # actions/checkout v6.0.2 - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - ref: ${{ github.sha }} - persist-credentials: false - - - name: Compute release metadata - id: meta - run: | - if [ "$GITHUB_REF" != "refs/heads/master" ]; then - echo \ - "Release Latest Assets may only be run from master" \ - >&2 - exit 1 - fi - release_tag="$( - gh api "repos/${GITHUB_REPOSITORY}/releases/latest" \ - --jq '.tag_name' - )" - binary_version="$( - awk -F'"' '/^const VERSION = "/ { print $2; exit }' main.go - )" - if [ -z "$binary_version" ]; then - echo "Could not determine binary version from main.go" >&2 - exit 1 - fi - if [ -z "$release_tag" ]; then - echo "Could not determine latest release tag" >&2 - exit 1 - fi - - { - echo "binary_version=${binary_version}" - echo "release_tag=${release_tag}" - echo "release_title=${release_tag}" - echo "release_notes_intro=Manual release build for current latest\ - release \`${release_tag}\` from \`${GITHUB_REF_NAME}\`." - echo "release_notes_summary=This run publishes assets to the repo's\ - current latest release tag." - echo "datafiles_archive=gomud-ALL-datafiles.zip" - echo "checksums_file=SHA256SUMS.txt" - } >> "$GITHUB_OUTPUT" - - build: - runs-on: ubuntu-24.04 - needs: prep - env: - BINARY_VERSION: ${{ needs.prep.outputs.binary_version }} - steps: - # actions/checkout v6.0.2 - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - ref: ${{ github.sha }} - persist-credentials: false - - uses: ./.github/actions/setup-go - - - name: Generate code - run: go generate ./... - - - name: Run tests - run: go test -race ./... - - - name: Create bin directory - run: mkdir -p bin/ - - - name: Copy _datafiles to bin/ - run: cp -r _datafiles bin/ - - - name: Build windows amd64 - run: >- - env GOOS=windows GOARCH=amd64 go build -v - -ldflags "-X main.version=${{ env.BINARY_VERSION }}" - -o bin/gomud-windows_x64.exe . - - - name: Build windows arm64 - run: >- - env GOOS=windows GOARCH=arm64 go build -v - -ldflags "-X main.version=${{ env.BINARY_VERSION }}" - -o bin/gomud-windows_arm64.exe . - - - name: Build darwin/arm64 - run: >- - env GOOS=darwin GOARCH=arm64 go build -v - -ldflags "-X main.version=${{ env.BINARY_VERSION }}" - -o bin/gomud-darwin_arm64 . - - - name: Build darwin/amd64 - run: >- - env GOOS=darwin GOARCH=amd64 go build -v - -ldflags "-X main.version=${{ env.BINARY_VERSION }}" - -o bin/gomud-darwin_x64 . - - - name: Build linux/amd64 - run: >- - env GOOS=linux GOARCH=amd64 go build -v - -ldflags "-X main.version=${{ env.BINARY_VERSION }}" - -o bin/gomud-linux_x64 . - - - name: Build linux/arm64 - run: >- - env GOOS=linux GOARCH=arm64 go build -v - -ldflags "-X main.version=${{ env.BINARY_VERSION }}" - -o bin/gomud-linux_arm64 . - - - name: Build linux/armv7 - run: >- - env GOOS=linux GOARCH=arm GOARM=7 go build -v - -ldflags "-X main.version=${{ env.BINARY_VERSION }}" - -o bin/gomud-linux_armv7 . - - - name: Upload bin - # actions/upload-artifact v7.0.1 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a - with: - name: bin-artifact - path: bin/ - - release: - runs-on: ubuntu-24.04 - permissions: - contents: write - needs: - - prep - - build - env: - RELEASE_TAG: ${{ needs.prep.outputs.release_tag }} - RELEASE_TITLE: ${{ needs.prep.outputs.release_title }} - BINARY_VERSION: ${{ needs.prep.outputs.binary_version }} - RELEASE_NOTES_INTRO: ${{ needs.prep.outputs.release_notes_intro }} - RELEASE_NOTES_SUMMARY: ${{ needs.prep.outputs.release_notes_summary }} - DATAFILES_ARCHIVE: ${{ needs.prep.outputs.datafiles_archive }} - CHECKSUMS_FILE: ${{ needs.prep.outputs.checksums_file }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - steps: - # actions/checkout v6.0.2 - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - ref: ${{ github.sha }} - persist-credentials: false - - - name: Download builds - # actions/download-artifact v8.0.1 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c - with: - name: bin-artifact - path: bin/ - - - name: Archive datafiles - run: >- - zip -r - bin/${{ env.DATAFILES_ARCHIVE }} - bin/_datafiles - - - name: Generate release checksums - run: | - cd bin - sha256sum \ - gomud-windows_x64.exe \ - gomud-windows_arm64.exe \ - gomud-darwin_arm64 \ - gomud-darwin_x64 \ - gomud-linux_x64 \ - gomud-linux_arm64 \ - gomud-linux_armv7 \ - "${{ env.DATAFILES_ARCHIVE }}" \ - > "${{ env.CHECKSUMS_FILE }}" - - - name: Write release notes - run: | - latest_release_tag="$( - gh api "repos/${GITHUB_REPOSITORY}/releases/latest" \ - --jq '.tag_name' \ - 2>/dev/null || true - )" - generate_notes_args=( - -f "tag_name=release-notes-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" - -f "target_commitish=${GITHUB_SHA}" - ) - if [ -n "$latest_release_tag" ]; then - generate_notes_args+=(-f "previous_tag_name=${latest_release_tag}") - changes_since_line="- Changes since: \`${latest_release_tag}\`" - else - changes_since_line="- Changes since: initial release history" - fi - generated_release_notes="$( - gh api \ - -X POST \ - "repos/${GITHUB_REPOSITORY}/releases/generate-notes" \ - "${generate_notes_args[@]}" \ - --jq '.body' - )" - published_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - cat > release-notes.md </dev/null 2>&1; then - gh api \ - -X PATCH \ - "repos/${GITHUB_REPOSITORY}/git/refs/tags/${RELEASE_TAG}" \ - -f "sha=${GITHUB_SHA}" \ - -F force=true - gh release upload "$RELEASE_TAG" "${assets[@]}" --clobber - gh release edit "$RELEASE_TAG" \ - --title "$RELEASE_TITLE" \ - --target "$GITHUB_SHA" \ - --notes-file release-notes.md - else - gh release create "$RELEASE_TAG" \ - "${assets[@]}" \ - --title "$RELEASE_TITLE" \ - --target "$GITHUB_SHA" \ - --notes-file release-notes.md - fi diff --git a/.github/workflows/run-test.yml b/.github/workflows/run-test.yml deleted file mode 100644 index acc00e937..000000000 --- a/.github/workflows/run-test.yml +++ /dev/null @@ -1,33 +0,0 @@ ---- -name: Run Tests - -"on": - push: - branches: - - master - pull_request: - types: - - opened - - synchronize - - reopened - - ready_for_review - -concurrency: - group: >- - ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - test: - runs-on: ubuntu-24.04 - timeout-minutes: 30 - steps: - # actions/checkout v6.0.2 - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - persist-credentials: false - - uses: ./.github/actions/setup-go - - uses: ./.github/actions/codegen-and-test diff --git a/.github/workflows/stable-release.yml b/.github/workflows/stable-release.yml new file mode 100644 index 000000000..bd05988f4 --- /dev/null +++ b/.github/workflows/stable-release.yml @@ -0,0 +1,259 @@ +--- +name: Stable Release + +"on": + workflow_dispatch: + inputs: + release_tag: + description: Stable semver tag to create, such as v0.10.0 + required: true + type: string + +# Stable releases are intentionally immutable. Re-running with an existing tag +# or release fails instead of replacing published assets. +concurrency: + group: ${{ github.workflow }}-${{ inputs.release_tag }} + cancel-in-progress: false + +permissions: + contents: read + +env: + DATAFILES_ARCHIVE: gomud-ALL-datafiles.zip + CHECKSUMS_FILE: SHA256SUMS.txt + +jobs: + validate-release: + name: Validate Stable Release Request + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: read + env: + RELEASE_TAG: ${{ inputs.release_tag }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + outputs: + binary_version: ${{ steps.meta.outputs.binary_version }} + steps: + # actions/checkout v6.0.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + persist-credentials: false + + - name: Validate stable release request + id: meta + run: | + set -euo pipefail + + semver_re='^v(0|[1-9][0-9]*)\.' + semver_re="${semver_re}(0|[1-9][0-9]*)\." + semver_re="${semver_re}(0|[1-9][0-9]*)$" + if ! printf '%s\n' "$RELEASE_TAG" | grep -Eq "$semver_re"; then + echo "release_tag must be a stable semver tag like v0.10.0" >&2 + exit 1 + fi + + if gh api \ + "repos/${GITHUB_REPOSITORY}/git/ref/tags/${RELEASE_TAG}" \ + >/dev/null 2>&1; then + echo "Tag ${RELEASE_TAG} already exists; stable releases are" \ + "immutable." >&2 + exit 1 + fi + + if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then + echo "Release ${RELEASE_TAG} already exists; stable releases are" \ + "immutable." >&2 + exit 1 + fi + + binary_version="$( + awk -F'"' '/^const VERSION = "/ { print $2; exit }' main.go + )" + if [ -z "$binary_version" ]; then + echo "Could not determine binary version from main.go" >&2 + exit 1 + fi + + echo "binary_version=${binary_version}" >> "$GITHUB_OUTPUT" + + test: + name: Validate Release Source + runs-on: ubuntu-24.04 + timeout-minutes: 30 + needs: validate-release + permissions: + contents: read + steps: + # actions/checkout v6.0.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + persist-credentials: false + + - uses: ./.github/actions/setup-go + + - uses: ./.github/actions/go-checks + + build: + name: Build Release Binaries + runs-on: ubuntu-24.04 + timeout-minutes: 20 + needs: + - validate-release + - test + permissions: + contents: read + env: + BINARY_VERSION: ${{ needs.validate-release.outputs.binary_version }} + steps: + # actions/checkout v6.0.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + persist-credentials: false + + - uses: ./.github/actions/setup-go + + - name: Generate code + run: go generate ./... + + - name: Build release binaries + run: .github/scripts/build-release-binaries.sh + + - name: Upload release binaries + # actions/upload-artifact v7.0.1 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: release-binaries + path: dist/* + if-no-files-found: error + + publish: + name: Publish Stable Release + runs-on: ubuntu-24.04 + timeout-minutes: 30 + needs: + - validate-release + - build + environment: stable-release + permissions: + contents: write + id-token: write + attestations: write + env: + RELEASE_TAG: ${{ inputs.release_tag }} + RELEASE_TITLE: ${{ inputs.release_tag }} + BINARY_VERSION: ${{ needs.validate-release.outputs.binary_version }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + # actions/checkout v6.0.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + persist-credentials: false + + - name: Recheck stable release immutability + # The first check catches bad manual inputs early. This second check + # runs after environment approval in case someone created the tag or + # release while the workflow was waiting. + run: | + set -euo pipefail + + if gh api \ + "repos/${GITHUB_REPOSITORY}/git/ref/tags/${RELEASE_TAG}" \ + >/dev/null 2>&1; then + echo "Tag ${RELEASE_TAG} already exists; stable releases are" \ + "immutable." >&2 + exit 1 + fi + + if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then + echo "Release ${RELEASE_TAG} already exists; stable releases are" \ + "immutable." >&2 + exit 1 + fi + + - name: Download release binaries + # actions/download-artifact v8.0.1 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: release-binaries + path: bin/ + + - name: Assemble release assets + run: .github/scripts/assemble-release-assets.sh + + - name: Write release notes + env: + RELEASE_KIND: stable + RELEASE_NOTES_FILE: release-notes.md + run: .github/scripts/release-notes.sh + + - name: Resolve attestation paths + id: release-assets + # Attest binaries and checksums. The datafiles zip is covered by the + # checksum file because it is assembled inside this workflow. + run: | + { + echo 'attestation_paths<> "$GITHUB_OUTPUT" + + - name: Attest release assets + # actions/attest-build-provenance v4.1.0 + # yamllint disable-line rule:line-length + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 + with: + subject-path: ${{ steps.release-assets.outputs.attestation_paths }} + + - name: Create stable draft release + id: draft + # Draft first so a failed upload does not expose a partial release. + run: | + set -euo pipefail + + release_id="$( + gh api \ + -X POST \ + "repos/${GITHUB_REPOSITORY}/releases" \ + -f "tag_name=${RELEASE_TAG}" \ + -f "target_commitish=${GITHUB_SHA}" \ + -f "name=${RELEASE_TITLE}" \ + -f "body=Stable release assets are uploading." \ + -f "make_latest=true" \ + -F draft=true \ + -F prerelease=false \ + --jq '.id' + )" + + echo "release_id=${release_id}" >> "$GITHUB_OUTPUT" + + - name: Upload stable release assets + run: | + set -euo pipefail + + mapfile -t assets < <( + .github/scripts/release-assets.sh upload-paths + ) + + gh release upload "$RELEASE_TAG" "${assets[@]}" + + - name: Attach stable release notes + run: | + set -euo pipefail + + gh release edit "$RELEASE_TAG" \ + --notes-file release-notes.md \ + --title "$RELEASE_TITLE" \ + --latest + + - name: Publish stable release + env: + RELEASE_ID: ${{ steps.draft.outputs.release_id }} + run: | + set -euo pipefail + + gh api \ + -X PATCH \ + "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" \ + -F draft=false \ + -F prerelease=false diff --git a/.github/workflows/update-go-toolchain.yml b/.github/workflows/update-go-toolchain.yml index 09dc16b65..62ba0b8cb 100644 --- a/.github/workflows/update-go-toolchain.yml +++ b/.github/workflows/update-go-toolchain.yml @@ -3,6 +3,7 @@ name: Update Go Toolchain "on": schedule: + # Monthly check near the end of the month, after typical Go patch releases. - cron: '0 6 25 * *' workflow_dispatch: @@ -55,6 +56,8 @@ jobs: } const latestVersion = stable.version.replace(/^go/, '') + // Keep the repository pinned in one place. Docker, CI, and helper + // scripts derive their Go version from go.mod. const updates = [ { path: 'go.mod', @@ -110,6 +113,8 @@ jobs: https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git run: | + set -euo pipefail + git config user.name "github-actions[bot]" git config user.email \ "41898282+github-actions[bot]@users.noreply.github.com" @@ -124,6 +129,9 @@ jobs: git add go.mod go.sum git commit -m \ "deps: update Go toolchain to ${GO_VERSION}" + + # Existing automation branches are updated with a lease so a manual + # maintainer edit cannot be overwritten silently. if [ -n "$remote_sha" ]; then git push \ --force-with-lease="$BRANCH_NAME:$remote_sha" \ diff --git a/Makefile b/Makefile index b41e012df..69a89178f 100644 --- a/Makefile +++ b/Makefile @@ -1,22 +1,41 @@ +# --- Makefile Overview --- +# - Run "make help" to show a full list of commands. +# - Comments marked with double hash signs ("##") will appear in `make help` output. +# - Most command values are overridable: `make build BIN=gomud VERSION=v1.2.3`. -.DEFAULT_GOAL := build +# --- Makefile Variables --- + +.DEFAULT_GOAL := help VERSION ?= $(shell git rev-parse HEAD) BIN ?= go-mud-server -DOCKER_COMPOSE := docker-compose -f compose.yml GO_VERSION ?= $(shell awk '/^toolchain go/ { sub(/^toolchain go/, ""); print; found=1; exit } /^go / && !gover { gover=$$2 } END { if (!found && gover) print gover }' go.mod) + +DOCKER_COMPOSE := docker-compose -f compose.yml GO_CONSOLE_IMAGE ?= golang:$(GO_VERSION)-bookworm +DOCKER_CMD ?= bash + CI_LOCAL_IMAGE ?= gomud-ci-local CI_LOCAL_UID ?= $(shell id -u) CI_LOCAL_GID ?= $(shell id -g) CI_LOCAL_DOCKER_SOCK_GID ?= $(shell stat -c '%g' /var/run/docker.sock 2>/dev/null || id -g) CI_LOCAL_HOME ?= /home/gomud CI_LOCAL_ACT_CACHE_DIR ?= $(PWD)/.git/.cache/act +ACT_FLAGS ?= --pull=false -P ubuntu-24.04=catthehacker/ubuntu:act-latest +ACT_DRYRUN_SECRETS ?= -s DISCORD_WEBHOOK_URL=https://example.invalid/webhook + JSHINT_VERSION ?= 2.13.6 -JS_LINT_PATHS := $(shell find _datafiles -name '*.js' -print) +VENDORED_JS_LINT_PATHS := _datafiles/html/admin/static/js/monaco/% +JS_LINT_PATHS := $(filter-out $(VENDORED_JS_LINT_PATHS),$(shell find _datafiles -name '*.js' -print)) WEBCLIENT_WINDOW_JS := $(shell find _datafiles/html/public/static/js/windows -name '*.js' -print) WEBCLIENT_BASE_JS := $(filter-out $(WEBCLIENT_WINDOW_JS),$(JS_LINT_PATHS)) +JSHINT := npx --yes --loglevel=error jshint@$(JSHINT_VERSION) +JSHINT_BASE_CMD := $(JSHINT) $(WEBCLIENT_BASE_JS) +JSHINT_WINDOWS_CMD := $(JSHINT) --config .jshintrc.webclient-windows $(WEBCLIENT_WINDOW_JS) + +CROSS_BUILD_CMD = env $(strip GOOS=$(CROSS_GOOS) GOARCH=$(CROSS_GOARCH) $(if $(CROSS_GOARM),GOARM=$(CROSS_GOARM))) go build -o $(CROSS_OUTPUT) + CI_LOCAL_RUN := docker run --rm \ --user "$(CI_LOCAL_UID):$(CI_LOCAL_GID)" \ --group-add "$(CI_LOCAL_DOCKER_SOCK_GID)" \ @@ -26,263 +45,207 @@ CI_LOCAL_RUN := docker run --rm \ -v "$(CI_LOCAL_ACT_CACHE_DIR)":"$(CI_LOCAL_HOME)/.cache/act" \ -w /work \ $(CI_LOCAL_IMAGE) -ACT_FLAGS ?= --pull=false -P ubuntu-24.04=catthehacker/ubuntu:act-latest -ACT_DRYRUN_SECRETS ?= -s DISCORD_WEBHOOK_URL=https://example.invalid/webhook export GOFLAGS := -mod=mod -## Build Targets - -.PHONY: go-version -go-version: ### Print the Go version pinned in go.mod. - @printf '%s\n' "$(GO_VERSION)" +# --- Makefile Commands --- -.PHONY: docker_build -docker_build: - GO_VERSION=$(GO_VERSION) TAG=$(VERSION) $(DOCKER_COMPOSE) build server +## Help +help: ## List documented Makefile targets. + @awk ' \ + BEGIN { FS = ":.*##"; printf "\nUsage: make \nExample: make build\n" } \ + /^## / { printf "\n\033[90;3m%s\033[0m\n", substr($$0, 4); next } \ + /^[[:alnum:]_.%/-]+:.*## / { printf " \033[93m%-24s\033[0m %s\n", $$1, $$2 } \ + ' $(MAKEFILE_LIST) + @printf "\n" -.PHONY: ci-local-image -ci-local-image: ### Build the local CI tool image. - docker build \ - --build-arg GO_VERSION=$(GO_VERSION) \ - -f .github/Dockerfile.act \ - -t $(CI_LOCAL_IMAGE) . +## Developer Workflow +.PHONY: build build_local generate module validate test coverage fmt fmtcheck vet mod js-lint -.PHONY: ci-local -ci-local: ci-local-image ### Run local CI validation in a container. - mkdir -p "$(CI_LOCAL_ACT_CACHE_DIR)" - $(CI_LOCAL_RUN) make ci-local-inner +build: validate build_local ## Validate the code and build ./$(BIN). -.PHONY: ci-local-inner -ci-local-inner: ### Run the local CI checks inside the CI tool image. - actionlint .github/workflows/*.yml - yamllint .github - $(MAKE) validate - $(MAKE) js-lint - act $(ACT_FLAGS) --dryrun push \ - -e .github/act/push_master.json \ - -W .github/workflows/lint.yml - act $(ACT_FLAGS) --dryrun pull_request \ - -e .github/act/pull_request.json \ - -W .github/workflows/lint.yml - act $(ACT_FLAGS) --dryrun pull_request \ - -e .github/act/pull_request.json \ - -W .github/workflows/run-test.yml - act $(ACT_FLAGS) --dryrun pull_request $(ACT_DRYRUN_SECRETS) \ - -e .github/act/pull_request.json \ - -W .github/workflows/discord-notify.yml - act $(ACT_FLAGS) --dryrun push \ - -e .github/act/push_master.json \ - -W .github/workflows/build-and-release.yml - act $(ACT_FLAGS) --dryrun push $(ACT_DRYRUN_SECRETS) \ - -e .github/act/push_master.json \ - -W .github/workflows/docker-package.yml - act $(ACT_FLAGS) --dryrun pull_request $(ACT_DRYRUN_SECRETS) \ - -e .github/act/pull_request.json \ - -W .github/workflows/docker-package.yml +build_local: generate ## Generate module imports and compile the local server binary. + @go mod tidy + CGO_ENABLED=0 go build -trimpath -a -o $(BIN) -DOCKER_CMD ?= bash +generate: ## Refresh generated module import wiring. + go generate -.PHONY: console -console: ### Open a shell in the project Go toolchain container. - @docker run --rm -it --init \ - -v "$(PWD)":/src \ - -w /src \ - $(GO_CONSOLE_IMAGE) \ - $(DOCKER_CMD) +# Pass module-manager arguments after the target: +# make module list +# make module install all-official +MODULE_ARGS := $(filter-out module,$(MAKECMDGOALS)) +module: ## Run the community module manager. + @go run . module $(MODULE_ARGS) + +ifneq ($(filter module,$(MAKECMDGOALS)),) +.PHONY: $(MODULE_ARGS) +$(MODULE_ARGS): + @: +endif -docker-%: - @$(MAKE) console DOCKER_CMD="make $(patsubst docker-%,%,$@)" +validate: fmtcheck vet ## Run the standard Go formatting and vet checks. -# -# -# For a complete list of GOOS/GOARCH combinations: -# Run: go tool dist list -# -# +test: generate js-lint ## Run code generation, JavaScript linting, and Go tests. + @go test -race ./... -.PHONY: build_rpi_zero2w -build_rpi_zero2w: generate ### Build a binary for a raspberry pi zero 2w - env GOOS=linux GOARCH=arm64 go build -o $(BIN)-rpi +coverage: ## Generate and open an HTML Go coverage report. + @mkdir -p bin/covdatafiles && \ + go test ./... -coverprofile=bin/covdatafiles/cover.out && \ + go tool cover -html=bin/covdatafiles/cover.out && \ + rm -rf bin -.PHONY: build_win64 -build_win64: generate ### Build a binary for 64bit windows - env GOOS=windows GOARCH=amd64 go build -o $(BIN)-win64.exe +fmt: ## Format all Go files. + @go fmt ./... -.PHONY: build_linux64 -build_linux64: generate ### Build a binary for linux - env GOOS=linux GOARCH=amd64 go build -o $(BIN)-linux64 +fmtcheck: ## Fail if any Go file is not gofmt-formatted. + @set -e; \ + unformatted=$$(gofmt -l $$(git ls-files '*.go')); \ + if [ -n "$$unformatted" ]; then \ + echo "Go files need formatting:"; \ + printf '%s\n' "$$unformatted"; \ + exit 1; \ + fi -.PHONY: build -build: validate build_local ### Validate the code and build the binary. +vet: ## Run go vet with repo-specific composite literal settings. + @go vet -composites=false ./... -.PHONY: build_local -build_local: generate +mod: ## Refresh vendored modules, tidy go.mod, and verify dependencies. + @go mod vendor @go mod tidy - CGO_ENABLED=0 go build -trimpath -a -o $(BIN) + @go mod verify -.PHONY: generate -generate: ### Generates include directives for modules - go generate +js-lint: ## Run JSHint using npx when available, otherwise Docker. + @if command -v npx >/dev/null 2>&1; then \ + $(JSHINT_BASE_CMD) && \ + $(JSHINT_WINDOWS_CMD); \ + elif command -v docker >/dev/null 2>&1; then \ + docker run --rm -v "$(PWD)":/app -w /app node:22 sh -lc "\ + $(JSHINT_BASE_CMD) && \ + $(JSHINT_WINDOWS_CMD)"; \ + else \ + echo "js-lint requires npx or docker" >&2; \ + exit 127; \ + fi -.PHONY: module -module: ### Manage community modules (usage: go run . module ) - @go run . module $(filter-out module,$(MAKECMDGOALS)) +## Running Locally +.PHONY: run run-new clean-instances https-setup reset-admin-pw client +run: generate ## Start the server with `go run .`. + @go run . -# Clean both development and production containers -.PHONY: clean -clean: - $(DOCKER_COMPOSE) down --volumes --remove-orphans - docker system prune -a +run-new: clean-instances generate run ## Delete room instance data and start a fresh world. -.PHONY: clean-instances -clean-instances: ### Deletes all room instance data. Starts the world fresh. +clean-instances: ## Delete generated room instance data for bundled worlds. rm -Rf _datafiles/world/default/rooms.instances rm -Rf _datafiles/world/empty/rooms.instances -## Run Targets +https-setup: ## Run the interactive HTTPS certificate setup helper. + @sh ./scripts/https-setup.sh -.PHONY: run -run: generate ### Build and run server. - @go run . +reset-admin-pw: ## Interactively reset the admin user's password. + @go run ./cmd/reset-admin-pw -.PHONY: run-new -run-new: clean-instances generate run ### Deletes instance data and runs server +client: ## Open a telnet client connected to the Docker server. + $(DOCKER_COMPOSE) run --rm terminal telnet go-mud-server 33333 + +## Docker And CI +.PHONY: docker_build run-docker console ci-local-image ci-local ci-local-inner clean + +docker_build: ## Build the server image with compose.yml. + GO_VERSION=$(GO_VERSION) TAG=$(VERSION) $(DOCKER_COMPOSE) build server -.PHONY: run-docker -run-docker: ### Build and run server in docker. +run-docker: ## Build and start the server container from compose.yml. GO_VERSION=$(GO_VERSION) $(DOCKER_COMPOSE) up --build --remove-orphans server -.PHONY: https-setup -https-setup: ### Interactive HTTPS certificate setup helper. - @sh ./scripts/https-setup.sh +console: ## Open a shell in a Go toolchain container mounted on this repo. + @docker run --rm -it --init \ + -v "$(PWD)":/src \ + -w /src \ + $(GO_CONSOLE_IMAGE) \ + $(DOCKER_CMD) -.PHONY: reset-admin-pw -reset-admin-pw: ### Interactively reset the admin user's password. - @go run ./cmd/reset-admin-pw +docker-%: ## Run a make target inside the Go toolchain container, for example `make docker-test`. + @$(MAKE) console DOCKER_CMD="make $(patsubst docker-%,%,$@)" +ci-local-image: ## Build the local CI tool image used by `make ci-local`. + docker build \ + --build-arg GO_VERSION=$(GO_VERSION) \ + -f .github/Dockerfile.act \ + -t $(CI_LOCAL_IMAGE) . -.PHONY: client -client: ### Build and run client terminal client - $(DOCKER_COMPOSE) run --rm terminal telnet go-mud-server 33333 +ci-local: ci-local-image ## Run local CI validation in the CI tool container. + mkdir -p "$(CI_LOCAL_ACT_CACHE_DIR)" + $(CI_LOCAL_RUN) make ci-local-inner -.PHONY: image_tag -image_tag: - @echo $(VERSION) +ci-local-inner: ## Run CI checks from inside the local CI tool container. + actionlint .github/workflows/*.yml + yamllint .github + $(MAKE) validate + $(MAKE) js-lint + ACT_FLAGS="$(ACT_FLAGS)" \ + ACT_DRYRUN_SECRETS="$(ACT_DRYRUN_SECRETS)" \ + .github/scripts/ci-local-act.sh -.PHONY: port -port: - @$(eval PORT := $(shell $(DOCKER_COMPOSE) port server 8080)) - @echo $(PORT) +clean: ## Stop compose services, remove their volumes, and prune Docker images. + $(DOCKER_COMPOSE) down --volumes --remove-orphans + docker system prune -a -.PHONY: shell -shell: - @$(eval CONTAINER_NAME := $(shell docker ps --filter="name=mud" --format '{{.Names}}' )) - docker exec -it $(CONTAINER_NAME) /bin/sh - -# -# -# Local code run/test -# -# -.PHONY: validate -validate: fmtcheck vet - -.PHONY: test -test: generate js-lint ### Run code generation, lint, and Go tests. - @go test -race ./... +## Cross Builds +.PHONY: build_rpi_zero2w build_win64 build_linux64 -.PHONY: coverage -coverage: - @mkdir -p bin/covdatafiles && \ - go test ./... -coverprofile=bin/covdatafiles/cover.out && \ - go tool cover -html=bin/covdatafiles/cover.out && \ - rm -rf bin +# For supported GOOS/GOARCH values, run: go tool dist list +build_rpi_zero2w: CROSS_GOOS := linux +build_rpi_zero2w: CROSS_GOARCH := arm64 +build_rpi_zero2w: CROSS_OUTPUT := $(BIN)-rpi +build_rpi_zero2w: generate ## Build a Raspberry Pi Zero 2 W binary. -.PHONY: js-lint -js-lint: ### Run Javascript linter -# Grep filtering it to remove errors reported by docker image around npm packages -# if "### errors" is found in the output, exits with an error code of 1 -# This should allow us to use it in CI/CD. - @docker run --rm -v "$(PWD)":/app -w /app node:22 sh -lc "\ - npx --yes jshint@$(JSHINT_VERSION) $(WEBCLIENT_BASE_JS) && \ - npx --yes jshint@$(JSHINT_VERSION) --config .jshintrc.webclient-windows $(WEBCLIENT_WINDOW_JS)" \ - 2>&1 | grep -v "^npm " | tee /dev/stderr | grep -Eq "^[0-9]+ errors" && exit 1 || true - -# -# -# Cert generation for testing -# -# -.PHONY: cert-clean -cert-clean: - rm -f server.crt server.key +build_win64: CROSS_GOOS := windows +build_win64: CROSS_GOARCH := amd64 +build_win64: CROSS_OUTPUT := $(BIN)-win64.exe +build_win64: generate ## Build a 64-bit Windows binary. -.PHONY: cert -cert: server.crt server.key +build_linux64: CROSS_GOOS := linux +build_linux64: CROSS_GOARCH := amd64 +build_linux64: CROSS_OUTPUT := $(BIN)-linux64 +build_linux64: generate ## Build a 64-bit Linux binary. -# This rule generates both files in one go using OpenSSL. -server.crt server.key: - openssl req -x509 -nodes -newkey rsa:4096 \ - -keyout server.key -out server.crt \ - -days 365 -subj "/CN=localhost" +build_rpi_zero2w build_win64 build_linux64: + $(CROSS_BUILD_CMD) +## Utility +.PHONY: go-version image_tag port shell cert-clean cert set_gopath view_pprof_mem help +go-version: ## Print the Go version pinned in go.mod. + @printf '%s\n' "$(GO_VERSION)" -# Go targets +image_tag: ## Print the current Docker image tag value. + @echo $(VERSION) -.PHONY: fmt -fmt: - @go fmt ./... +port: ## Print the host port mapped to server port 8080. + @$(eval PORT := $(shell $(DOCKER_COMPOSE) port server 8080)) + @echo $(PORT) -.PHONY: fmtcheck -fmtcheck: fmt - @set -e; \ - unformatted=$$(go fmt ./...); \ - if [ ! -z "$$unformatted" ]; then \ - echo Fixed inconsistent format in some files.; \ - echo $$unformatted; \ - exit 1; \ - fi +shell: ## Open /bin/sh inside the running server container. + @$(eval CONTAINER_NAME := $(shell docker ps --filter="name=mud" --format '{{.Names}}' )) + docker exec -it $(CONTAINER_NAME) /bin/sh -.PHONY: mod -mod: - @go mod vendor - @go mod tidy - @go mod verify +cert-clean: ## Remove local development TLS certificate files. + rm -f server.crt server.key +cert: server.crt server.key ## Generate local self-signed TLS certificate files. -.PHONY: vet -vet: - @go vet -composites=false ./... +server.crt server.key: + openssl req -x509 -nodes -newkey rsa:4096 \ + -keyout server.key -out server.crt \ + -days 365 -subj "/CN=localhost" -.PHONY: set_gopath -set_gopath: +set_gopath: ## Print a command for adding this repo to GOPATH in legacy shells. ifeq ($(OS),Windows_NT) - setx PATH "$(PATH);mytest" -m + @echo 'PowerShell: $$env:GOPATH = "$$env:GOPATH;$(CURDIR)"' else - export GOPATH=$GOPATH:$(pwd) + @printf 'export GOPATH="$${GOPATH:+$$GOPATH:}%s"\n' "$(CURDIR)" endif -.PHONY: view_pprof_mem -view_pprof_mem: +view_pprof_mem: ## Open the saved memory profile in the Go pprof web UI. go tool pprof -http=:8989 source/_datafiles/profiles/mem.pprof - -# -# Help target - greps and formats special comments to form a "help" command for makefiles -# -## Help -.PHONY: help -help: ### List makefile targets. -# 1. grep for any lines starting with "##" or containing "\s###\s" -# 2. Align targets/comments with string padding -# 3. Wrap lines starting with "##" in ANSI escape codes (color) as headers -# 4. Wrap lines containing "\s###\s" in ANSI escape codes (color) as commands -# 5. Add new lines before any that aren't prefixed with space (Headers) - @grep -hE "^##\s|\s###\s" $(MAKEFILE_LIST) \ - | awk -F'[[:space:]]###[[:space:]]' '{printf "%-36s### %s\n", substr($$1,1,35), $$2}' \ - | sed -E "s/^## ([^#]*)#*/`printf "\033[90;3m"`\1`printf "\033[0m"`/" \ - | sed "s/\(.*\):\(.*\)###\(.*\)$$/ `printf "\033[93m"`\1:`printf "\033[36m"`\2`printf "\033[97m"`-\3`printf "\033[0m"`/" \ - | sed "/^[^[:blank:]]/{x;p;x;}" - @printf "\n" diff --git a/main.go b/main.go index 98b326f3a..609a4fdb4 100644 --- a/main.go +++ b/main.go @@ -64,7 +64,7 @@ import ( // When updating this version: // 1. Expect to update the github release version // 2. Consider whether any migration code is needed for breaking changes, particularly in datafiles (see internal/migration) -const VERSION = "0.9.2" +const VERSION = "0.9.10" var ( sigChan = make(chan os.Signal, 1)