Skip to content

Create Makefile & release pipeline, make git-bundle-server web-server more resilient #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Feb 9, 2023
138 changes: 138 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
name: Release

on:
push:
tags:
- 'v[0-9]*.[0-9]*.[0-9]*' # matches "v<number...>.<number...>.<number>..."

jobs:
# Check prerequisites for the workflow
prereqs:
name: Prerequisites
runs-on: ubuntu-latest
outputs:
tag_name: ${{ steps.tag.outputs.name }} # The full name of the tag, e.g. v1.0.0
tag_version: ${{ steps.tag.outputs.version }} # The version number (without preceding "v"), e.g. 1.0.0
steps:
- name: Determine tag to build
run: |
echo "name=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
id: tag

package:
needs: prereqs
name: ${{matrix.jobs.jobname}}
strategy:
fail-fast: false
matrix:
jobs:
- jobname: Create MacOS .pkg (x86_64)
goarch: amd64
pool: macos-latest
artifact: _dist/*.pkg
- jobname: Create MacOS .pkg (ARM64)
goarch: arm64
pool: macos-latest
artifact: _dist/*.pkg
- jobname: Create binary Debian package (x86_64)
goarch: amd64
pool: ubuntu-latest
artifact: _dist/*.deb
env:
GOARCH: ${{matrix.jobs.goarch}}
runs-on: ${{matrix.jobs.pool}}
steps:
- name: Setup Go
uses: actions/setup-go@v3
with:
go-version: '1.19.0'
- name: Clone repository
uses: actions/checkout@v3
- name: Build the release artifact
run: make package VERSION=${{ needs.prereqs.outputs.tag_version }}
- name: Get the release artifact
shell: bash
run: |
artifacts=(${{matrix.jobs.artifact}})

# Get path to, and name of, artifact
artifactPath="${artifacts[0]}"
artifactName=$(basename "$artifactPath")

# Export variables to environment
echo "artifactPath=$artifactPath" >> $GITHUB_ENV
echo "artifactName=$artifactName" >> $GITHUB_ENV
- name: Upload release artifact
uses: actions/upload-artifact@v3
with:
name: ${{env.artifactName}}
path: ${{github.workspace}}/${{env.artifactPath}}
if-no-files-found: error

create-github-release:
needs: [prereqs, package]
name: Create release with artifacts
runs-on: ubuntu-latest
steps:
- name: Download packages
uses: actions/download-artifact@v3
with:
path: artifacts-raw
- name: Consolidate artifact directory
shell: bash
run: |
# This step is needed to extract the artifacts from their wrapper
# parent directories. For more details, see
# https://github.com/actions/download-artifact#download-all-artifacts
mkdir artifacts
mv artifacts-raw/*/* artifacts/
- name: Create release & attach artifacts
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
const path = require('path');

var releaseMetadata = {
owner: context.repo.owner,
repo: context.repo.repo
};

// Create the release
var tagName = "${{ needs.prereqs.outputs.tag_name }}";
var createdRelease = await github.rest.repos.createRelease({
...releaseMetadata,
draft: true,
tag_name: tagName,
name: tagName,
generate_release_notes: true
Copy link
Collaborator

Choose a reason for hiding this comment

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

Spiffy!

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm looking forward to using the generated release notes in a new project. Having all of the releases match this generated format should help, as opposed to other places where an established release note pattern doesn't match the generated notes.

});
releaseMetadata.release_id = createdRelease.data.id;

// Upload contents of directory to the release created above
async function uploadDirectoryToRelease(directory, includeExtensions=[]) {
return fs.promises.readdir(directory)
.then(async(files) => Promise.all(
files.filter(file => {
return includeExtensions.length==0 || includeExtensions.includes(path.extname(file).toLowerCase());
})
.map(async (file) => {
var filePath = path.join(directory, file);
return github.rest.repos.uploadReleaseAsset({
...releaseMetadata,
name: file,
headers: {
"content-length": (await fs.promises.stat(filePath)).size
},
data: fs.createReadStream(filePath)
});
}))
);
}

await Promise.all([
// Upload all artifacts
uploadDirectoryToRelease('artifacts', ['.pkg', '.deb'])
]);

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
/git-bundle-server
/git-bundle-web-server
/bin/
/_dist/
20 changes: 19 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,25 @@
"editor.wordWrap": "off",
"files.trimTrailingWhitespace": true,
},
"[makefile]": {
"editor.detectIndentation": false,
"editor.insertSpaces": false,
"editor.tabSize": 8,
"editor.wordWrap": "off",
"files.trimTrailingWhitespace": true,
},
"[shellscript]": {
"editor.detectIndentation": false,
"editor.insertSpaces": false,
"editor.tabSize": 8,
"editor.wordWrap": "off",
"files.trimTrailingWhitespace": true,
},
Comment on lines +9 to +22
Copy link
Collaborator

Choose a reason for hiding this comment

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

Good guardrails

"files.associations": {
"*.md": "markdown"
"*.md": "markdown",
"*.sh": "shellscript",
"prerm": "shellscript",
"postinstall": "shellscript",
"Makefile": "makefile"
}
}
113 changes: 113 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Default target
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nice! This Makefile is really simple and lovely.

build:

# Project metadata (note: to package, VERSION *must* be set by the caller)
NAME := git-bundle-server
VERSION :=
PACKAGE_REVISION := 1

# Helpful paths
BINDIR := $(CURDIR)/bin
DISTDIR := $(CURDIR)/_dist

# Platform information
GOOS := $(shell go env GOOS)
GOARCH := $(shell go env GOARCH)
Comment on lines +13 to +15
Copy link
Collaborator

Choose a reason for hiding this comment

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

These are set from go env here, but you mention they could be modified later. I'll keep an eye out for that.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Revisiting this: I was incorrect. You are not manually overriding these, but using them to decide which kind of installer is necessary. I was expecting this to need to change to build macOS ARM binaries on intel machines, but I don't see that changing in those workflows.


# Packaging information
SUPPORTED_PACKAGE_GOARCHES := amd64 arm64
PACKAGE_ARCH := $(GOARCH)

# Build targets
.PHONY: build
build:
$(RM) -r $(BINDIR)
@mkdir -p $(BINDIR)
GOOS="$(GOOS)" GOARCH="$(GOARCH)" go build -o $(BINDIR) ./...

# Packaging targets
.PHONY: check-arch
check-arch:
$(if $(filter $(GOARCH),$(SUPPORTED_PACKAGE_GOARCHES)), , \
$(error cannot create package for GOARCH "$(GOARCH)"; \
supported architectures are: $(SUPPORTED_PACKAGE_GOARCHES)))

.PHONY: check-version
check-version:
$(if $(VERSION), , $(error version is undefined))

ifeq ($(GOOS),linux)
# Linux binary .deb file
# Steps:
# 1. Layout files in _dist/deb/root/ as they'll be installed (unlike MacOS
# .pkg packages, symlinks created in the payload are preserved, so we
# create them here to avoid doing so in a post-install step).
# 2. Create the binary deb package in _dist/deb/.

# Platform-specific variables
DEBDIR := $(DISTDIR)/deb
DEB_FILENAME := $(DISTDIR)/$(NAME)_$(VERSION)-$(PACKAGE_REVISION)_$(PACKAGE_ARCH).deb

# Targets
$(DEBDIR)/root: check-arch build
@echo
@echo "======== Formatting package contents ========"
@build/package/layout-unix.sh --bindir="$(BINDIR)" \
--include-symlinks \
--output="$(DEBDIR)/root"

$(DEB_FILENAME): check-version $(DEBDIR)/root
@echo
@echo "======== Creating binary Debian package ========"
@build/package/deb/pack.sh --payload="$(DEBDIR)/root" \
--scripts="$(CURDIR)/build/package/deb/scripts" \
--arch="$(PACKAGE_ARCH)" \
--version="$(VERSION)" \
--output="$(DEB_FILENAME)"

.PHONY: package
package: $(DEB_FILENAME)

else ifeq ($(GOOS),darwin)
# MacOS .pkg file
# Steps:
# 1. Layout files in _dist/pkg/payload/ as they'll be installed (including
# uninstall.sh script).
# 2. Create the product archive in _dist/.

# Platform-specific variables
PKGDIR := $(DISTDIR)/pkg
PKG_FILENAME := $(DISTDIR)/$(NAME)_$(VERSION)-$(PACKAGE_REVISION)_$(PACKAGE_ARCH).pkg

# Targets
$(PKGDIR)/payload: check-arch build
@echo
@echo "======== Formatting package contents ========"
@build/package/layout-unix.sh --bindir="$(BINDIR)" \
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just noticing that you're able to share a lot in this script between Linux and macOS. layout-unix is a good name for that.

--uninstaller="$(CURDIR)/build/package/pkg/uninstall.sh" \
--output="$(PKGDIR)/payload"

$(PKG_FILENAME): check-version $(PKGDIR)/payload
@echo
@echo "======== Creating product archive package ========"
@build/package/pkg/pack.sh --version="$(VERSION)" \
--payload="$(PKGDIR)/payload" \
--output="$(PKG_FILENAME)"

.PHONY: package
package: $(PKG_FILENAME)

else
# Packaging not supported for platform, exit with error.
.PHONY: package
package:
$(error cannot create package for GOOS "$(GOOS)")

endif

# Cleanup targets
.PHONY: clean
clean:
go clean ./...
$(RM) -r $(BINDIR)
$(RM) -r $(DISTDIR)
93 changes: 93 additions & 0 deletions build/package/deb/pack.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!/bin/bash
die () {
echo "$*" >&2
exit 1
}

# Directories
THISDIR="$( cd "$(dirname "$0")" ; pwd -P )"

# Local paths
UNINSTALLER="$THISDIR/uninstall.sh"

# Parse script arguments
for i in "$@"
do
case "$i" in
--payload=*)
DEBROOT="${i#*=}"
shift # past argument=value
;;
--scripts=*)
SCRIPT_DIR="${i#*=}"
shift # past argument=value
;;
--arch=*)
ARCH="${i#*=}"
shift # past argument=value
;;
--version=*)
VERSION="${i#*=}"
shift # past argument=value
;;
--output=*)
DEBOUT="${i#*=}"
shift # past argument=value
;;
*)
die "unknown option '$i'"
;;
esac
done

# Perform pre-execution checks
if [ -z "$DEBROOT" ]; then
die "--payload was not set"
elif [ ! -d "$DEBROOT" ]; then
die "Could not find '$DEBROOT'. Did you run layout-unix.sh first?"
fi
if [ -z "$ARCH" ]; then
die "--arch was not set"
fi
if [ -z "$VERSION" ]; then
die "--version was not set"
fi
if [ -z "$DEBOUT" ]; then
die "--output was not set"
fi

# Exit as soon as any line fails
set -e

# Cleanup old package
if [ -e "$DEBOUT" ]; then
echo "Deleting old package '$DEBOUT'..."
rm -f "$DEBOUT"
fi

CONTROLDIR="$DEBROOT/DEBIAN"

# Ensure the parent directory for the .deb exists
mkdir -p "$(dirname "$DEBOUT")"

# Build .deb
mkdir -m 755 -p "$CONTROLDIR"

# Create the debian control file
cat >"$CONTROLDIR/control" <<EOF
Package: git-bundle-server
Version: $VERSION
Section: vcs
Priority: optional
Architecture: $ARCH
Depends:
Maintainer: Git Bundle Server <[email protected]>
Description: A self-hostable Git bundle server.
EOF

# Copy the maintainer scripts, if they exist
if [ -d "$SCRIPT_DIR" ]; then
cp -R "$SCRIPT_DIR/." "$CONTROLDIR"
fi

dpkg-deb -Zxz --build "$DEBROOT" "$DEBOUT"
12 changes: 12 additions & 0 deletions build/package/deb/scripts/prerm
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/bash
set -e

# Stop & cleanup the web server as the logged-in user
# The XDG_RUNTIME_DIR is required for 'systemctl' to work, so we manually set it
# to that of the logged-in user.
LOGGED_IN_USER="${SUDO_USER:-${USER}}"
LOGGED_IN_UID="$(sudo -u $LOGGED_IN_USER id -u)"
sudo -u $LOGGED_IN_USER XDG_RUNTIME_DIR=/run/user/$LOGGED_IN_UID \
/usr/local/git-bundle-server/bin/git-bundle-server web-server stop --remove

exit 0
Loading