From af747638ba6dd848889046065f9431d7caa80afd Mon Sep 17 00:00:00 2001 From: Blake Gentry Date: Tue, 27 Aug 2024 20:19:05 -0500 Subject: [PATCH] module release CI setup (#127) --- .dockerignore | 1 + .github/workflows/{docker.yml => docker.yaml} | 0 .github/workflows/package-and-release.yaml | 180 ++++++++++++++++++ .github/workflows/release.yaml | 27 +++ .gitignore | 1 + packager/go.mod | 7 + packager/go.sum | 4 + packager/packager.go | 117 ++++++++++++ 8 files changed, 337 insertions(+) rename .github/workflows/{docker.yml => docker.yaml} (100%) create mode 100644 .github/workflows/package-and-release.yaml create mode 100644 .github/workflows/release.yaml create mode 100644 packager/go.mod create mode 100644 packager/go.sum create mode 100644 packager/packager.go diff --git a/.dockerignore b/.dockerignore index 6cb8294..016c1dd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,3 +3,4 @@ dist Dockerfile node_modules +packager diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yaml similarity index 100% rename from .github/workflows/docker.yml rename to .github/workflows/docker.yaml diff --git a/.github/workflows/package-and-release.yaml b/.github/workflows/package-and-release.yaml new file mode 100644 index 0000000..a40c98c --- /dev/null +++ b/.github/workflows/package-and-release.yaml @@ -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 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..0f8803f --- /dev/null +++ b/.github/workflows/release.yaml @@ -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 }} diff --git a/.gitignore b/.gitignore index ae58b8a..52e2b0f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .env .env.* !.env.example +/packager/packager .tool-versions /riverui diff --git a/packager/go.mod b/packager/go.mod new file mode 100644 index 0000000..fb0a6d4 --- /dev/null +++ b/packager/go.mod @@ -0,0 +1,7 @@ +module riverqueue.com/riverqueue/packager + +go 1.23 + +toolchain go1.23.0 + +require golang.org/x/mod v0.18.0 diff --git a/packager/go.sum b/packager/go.sum new file mode 100644 index 0000000..84158d3 --- /dev/null +++ b/packager/go.sum @@ -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= diff --git a/packager/packager.go b/packager/packager.go new file mode 100644 index 0000000..69962bc --- /dev/null +++ b/packager/packager.go @@ -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(×tampInt, "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 +}