Skip to content
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

module release CI setup #127

Merged
merged 1 commit into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
dist
Dockerfile
node_modules
packager
File renamed without changes.
180 changes: 180 additions & 0 deletions .github/workflows/package-and-release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
name: Package and Release

on:
workflow_call:
inputs:
after-version:
required: false
type: string
module-base:
required: true
type: string
module-dir:
required: true
type: string
storage-bucket:
required: true
type: string
version-tag:
required: true
type: string
secrets:
r2-access-key-id:
description: "Cloudflare R2 access key ID passed from caller workflow"
required: true
r2-endpoint-url:
description: "Cloudflare R2 jurisdiction-specific endpoint URL"
required: true
r2-secret-access-key:
description: "A secret access key passed from the caller workflow"
required: true

jobs:
package:
name: Package
runs-on: ubuntu-latest
env:
AWS_DEFAULT_REGION: auto
AWS_ACCESS_KEY_ID: ${{ secrets.r2-access-key-id }}
AWS_ENDPOINT_URL: ${{ secrets.r2-endpoint-url }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.r2-secret-access-key }}
OUTPUT_DIR: /tmp/mod-zip-result

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Install Node.js
uses: actions/setup-node@v4
with:
cache: "npm"
cache-dependency-path: package-lock.json
node-version: "20.12.2"

- name: Install dependencies
run: npm install
shell: sh

- name: Build JS 🏗️
run: npm exec vite build

- name: Setup Go
uses: actions/setup-go@v5
with:
check-latest: true
go-version-file: "packager/go.mod"
cache-dependency-path: |
packager/go.sum

- name: Display Go version
run: go version

- name: Install dependencies
working-directory: ./packager
run: |
echo "::group::go get"
go get -t ./...
echo "::endgroup::"

- name: Build packager
working-directory: ./packager
run: go install .

- name: Fetch tags
run: git fetch --tags -f origin

- name: Determine module names and version details
id: module_info
run: |
BASE_MODULE_NAME=${{ inputs.module-base }}
MODULE_NAME=$(go list -m)
echo "base_module_name=${BASE_MODULE_NAME}" >> $GITHUB_OUTPUT
echo "module_name=${MODULE_NAME}" >> $GITHUB_OUTPUT
if [ "$MODULE_NAME" == "$BASE_MODULE_NAME" ]; then
TAG_PREFIX=""
else
TAG_PREFIX=${MODULE_NAME#$BASE_MODULE_NAME/}/
fi
echo "tag_prefix=${TAG_PREFIX}" >> $GITHUB_OUTPUT
VERSION_TAG=${{ inputs.version-tag }}
echo "version_tag=${VERSION_TAG}" >> $GITHUB_OUTPUT
VERSION_NUMBER=${VERSION_TAG#$TAG_PREFIX}
echo "version_number=${VERSION_NUMBER}" >> $GITHUB_OUTPUT
working-directory: ${{ inputs.module-dir }}

- name: Extract timestamp
id: extract_timestamp
run: echo "timestamp=$(git log -1 --format=%ct ${{ steps.module_info.outputs.version_tag }})" >> $GITHUB_OUTPUT

- name: print stuff
run: |
echo "base module: ${{ steps.module_info.outputs.base_module_name }}"
echo "module: ${{ steps.module_info.outputs.module_name }}"
echo "tag_prefix: ${{ steps.module_info.outputs.tag_prefix }}"
echo "timestamp: ${{ steps.extract_timestamp.outputs.timestamp }}"
echo "version_tag: ${{ steps.module_info.outputs.version_tag }}"
echo "version_number: ${{ steps.module_info.outputs.version_number }}"

- name: Remove all undesirable files
# Remove all files that we don't want to end up in the final mod release .zip file, including:
# * node_modules
# * UI source files, .json, .js
run: |
rm -r .eslintrc.cjs .github .storybook .stylelintignore .vscode node_modules package.json package-lock.json public src
find . -type f -name '*.json' -exec rm -f {} +

- name: Package module
# if: startsWith(github.ref, 'refs/tags/')
run: |
packager \
-dir ${{ inputs.module-dir }} \
-output ${{ env.OUTPUT_DIR }} \
-mod ${{ steps.module_info.outputs.module_name }} \
-timestamp ${{ steps.extract_timestamp.outputs.timestamp }} \
-version ${{ steps.module_info.outputs.version_number }}

- name: Extract sorted version list and write files
id: version_list
run: |
AFTER_VERSION="${{ inputs.after-version }}"
TAG_PREFIX="${{ steps.module_info.outputs.tag_prefix }}"
TAG_MATCHER="${TAG_PREFIX}v*"

if [ -n "$AFTER_VERSION" ]; then
VERSION_LIST=$(git tag -l "$TAG_MATCHER" | sed "s#^$TAG_PREFIX##" | sort -V | awk -v threshold="$AFTER_VERSION" '$0 > threshold')
else
VERSION_LIST=$(git tag -l "$TAG_MATCHER" | sed "s#^$TAG_PREFIX##" | sort -V)
fi

LATEST_VERSION=$(echo "$VERSION_LIST" | tail -n 1)

echo "Latest version: $LATEST_VERSION"
echo "All versions:"
echo "$VERSION_LIST"

if [ -z "$VERSION_LIST" ]; then
echo "Error: Version list is empty."
exit 1
fi

if [ -z "$LATEST_VERSION" ]; then
echo "Error: Latest version is empty."
exit 1
fi

echo "latest_version=${LATEST_VERSION}" >> $GITHUB_OUTPUT
echo "$VERSION_LIST" > ${{ env.OUTPUT_DIR }}/${{ steps.module_info.outputs.module_name }}/@v/list

- name: List results
run: |
tree ${{ env.OUTPUT_DIR }}
ls -la ${{ env.OUTPUT_DIR }}/${{ steps.module_info.outputs.module_name }}
ls -la ${{ env.OUTPUT_DIR }}/${{ steps.module_info.outputs.module_name }}/@v
cat ${{ env.OUTPUT_DIR }}/${{ steps.module_info.outputs.module_name }}/@v/list

- name: Sync files to R2
run: |
aws s3 cp ${{ env.OUTPUT_DIR }}/${{ steps.module_info.outputs.module_name }}/@v/ s3://${{ inputs.storage-bucket }}/${{ steps.module_info.outputs.module_name }}/@v/ --recursive
aws s3 cp s3://${{ inputs.storage-bucket }}/${{ steps.module_info.outputs.module_name }}/@v/${{ steps.version_list.outputs.latest_version }}.info s3://${{ inputs.storage-bucket }}/${{ steps.module_info.outputs.module_name }}/@latest
27 changes: 27 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Release

on:
push:
branches:
- master
- "*"
tags:
# Additional packages must be added both here AND in the job list below:
- "v*"

jobs:
release_riverui:
uses: ./.github/workflows/package-and-release.yaml
if: startsWith(github.ref, 'refs/tags/v')
with:
after-version: v0.4.0
module-base: riverqueue.com/riverui
module-dir: .
storage-bucket: ${{ vars.RELEASE_STORAGE_BUCKET }}
version-tag: ${{ github.ref_name}}
permissions:
contents: read
secrets:
r2-access-key-id: ${{ secrets.R2_ACCESS_KEY_ID }}
r2-endpoint-url: ${{ secrets.R2_ENDPOINT_URL }}
r2-secret-access-key: ${{ secrets.R2_SECRET_ACCESS_KEY }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.env
.env.*
!.env.example
/packager/packager
.tool-versions
/riverui

Expand Down
7 changes: 7 additions & 0 deletions packager/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module riverqueue.com/riverqueue/packager

go 1.23

toolchain go1.23.0

require golang.org/x/mod v0.18.0
4 changes: 4 additions & 0 deletions packager/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
117 changes: 117 additions & 0 deletions packager/packager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package main

import (
"encoding/json"
"errors"
"flag"
"log"
"os"
"path/filepath"
"time"

"golang.org/x/mod/module"
"golang.org/x/mod/semver"
"golang.org/x/mod/zip"
)

func main() {
if err := createBundle(); err != nil {
log.Fatal(err)
}
}

func createBundle() error {
var (
dir string
mod string
outputDirPath string
timestampInt int64
versionString string
)
flag.StringVar(&dir, "dir", "", "dir to package up")
flag.StringVar(&mod, "mod", "", "module name, i.e. github.com/user/repo")
flag.Int64Var(&timestampInt, "timestamp", 0, "timestamp of the version")
flag.StringVar(&outputDirPath, "output", "", "output directory path, which will include a fully nested directory structure for the module name")
flag.StringVar(&versionString, "version", "", "version of the module")

flag.Parse()

if dir == "" {
return errors.New("dir is required")
}
if outputDirPath == "" {
return errors.New("output is required")
}
if mod == "" {
return errors.New("mod is required")
}
if timestampInt == 0 {
return errors.New("timestamp is required")
}
if versionString == "" {
return errors.New("version is required")
}

if !semver.IsValid(versionString) {
return errors.New("version is not valid")
}

// Convert the timestamp to a time.Time object:
timestamp := time.Unix(timestampInt, 0).UTC()

nestedOutputDir := filepath.Join(outputDirPath, mod)
vOutputDir := filepath.Join(nestedOutputDir, "@v")

if err := os.MkdirAll(vOutputDir, 0o700); err != nil {
return err
}

version := module.Version{
Path: mod,
Version: versionString,
}

modFilename := version.Version + ".mod"
zipFilename := version.Version + ".zip"

modFileContents, err := os.ReadFile(filepath.Join(dir, "go.mod"))
if err != nil {
return err
}

if err := os.WriteFile(filepath.Join(vOutputDir, modFilename), modFileContents, 0o644); err != nil {
return err
}

f, err := os.OpenFile(filepath.Join(vOutputDir, zipFilename), os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return err
}
defer f.Close()

if err := zip.CreateFromDir(f, version, dir); err != nil {
return err
}

info := Info{
Version: version.Version,
Time: timestamp,
}

infoFile, err := os.Create(filepath.Join(vOutputDir, version.Version+".info"))
if err != nil {
return err
}
defer infoFile.Close()

if err := json.NewEncoder(infoFile).Encode(info); err != nil {
return err
}

return nil
}

type Info struct {
Version string // version string
Time time.Time // commit time
}
Loading