👷 rewrite CI build system

This commit is contained in:
Louis Orleans 2023-08-31 15:57:47 -07:00
parent 350efafa5b
commit de768d12c1
No known key found for this signature in database
4 changed files with 248 additions and 46 deletions

View File

@ -1,67 +1,72 @@
name: 'Publish Container Image' name: 'Build & Publish Container Image'
# Controls when the action will run. Triggers the workflow on push or pull
# request events but only for the master branch
on: on:
push: push:
branches: pull_request:
- '*'
permissions: permissions:
contents: 'read' contents: 'read'
packages: 'write'
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs: jobs:
# This workflow contains a single job called "build" test-scripts:
build-and-publish:
# The type of runner that the job will run on
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'
# Steps represent a sequence of tasks that will be executed as part of the job
steps: steps:
- name: 'Checkout Repository 🛎️' - name: 'Checkout Repository 🛎️'
uses: 'actions/checkout@v2' uses: 'actions/checkout@v3'
- name: 'Test 🧪'
run: 'ruby .github/workflows/scripts/test/get-image-tags.unit.rb'
- name: 'Build 🏗️' container-build:
id: 'build' needs: 'test-scripts'
runs-on: 'ubuntu-latest'
steps:
- name: 'Checkout Repository 🛎️'
uses: 'actions/checkout@v3'
- uses: 'docker/setup-buildx-action@v2'
- name: 'Get image tag names 🏷️'
id: 'tag-image'
run: | run: |
image="$( \ echo "image_tags=$(
basename "$(echo "${{ github.repository }}")" \ .github/workflows/scripts/get-image-tags.rb \
| tr '[:upper:]' '[:lower:]' \ "${{ github.repository }}" \
| sed 's/docker-//' \ "${{ github.ref_name }}" \
)" "${{ github.ref_type }}" \
echo "image=$image" >> "$GITHUB_OUTPUT" "${{ github.event.repository.default_branch }}" \
)" >> "$GITHUB_OUTPUT"
- name: 'Build container 🐳'
uses: 'docker/build-push-action@v3'
with:
file: 'Dockerfile'
tags: '${{ steps.tag-image.outputs.image_tags }}'
cache-from: 'type=gha'
cache-to: 'type=gha,mode=max'
docker build --file "Dockerfile" --tag "$image" . outputs:
image_tags: '${{ steps.tag-image.outputs.image_tags }}'
publish:
needs:
- 'container-build'
if: contains(needs.container-build.outputs.image_tags, ':latest')
runs-on: 'ubuntu-latest'
permissions:
packages: 'write'
steps:
- name: 'Checkout Repository 🛎️'
uses: 'actions/checkout@v3'
- uses: 'docker/setup-buildx-action@v2'
- name: 'Login to GitHub Container Registry 🔑' - name: 'Login to GitHub Container Registry 🔑'
uses: 'docker/login-action@v1' uses: 'docker/login-action@v2'
if: '${{ github.ref_name }} == ${{ github.event.repository.default_branch }}'
with: with:
registry: 'ghcr.io' registry: 'ghcr.io'
username: '${{ github.repository_owner }}' username: '${{ github.repository_owner }}'
password: '${{ secrets.GITHUB_TOKEN }}' password: '${{ secrets.GITHUB_TOKEN }}'
- name: 'Publish to Registry 🐳' - name: 'Publish to Registry 💨'
if: '${{ github.ref_name }} == ${{ github.event.repository.default_branch }}' uses: 'docker/build-push-action@v3'
run: | with:
image="${{ steps.build.outputs.image }}" file: 'Dockerfile'
repo="ghcr.io/${{ github.repository_owner }}/$image" push: true
branch="$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')" tags: '${{ needs.container-build.outputs.image_tags }}'
version="$branch" cache-from: 'type=gha'
echo "repo = $repo"
echo "image = $image"
echo "version = $version"
docker tag "$image" "$repo/$image:$version"
docker push "$repo/$image:$version"
# Use Docker `latest` tag convention
if [[ "${{ github.event.repository.default_branch }}" == "$branch" ]]; then
docker tag "$image" "$repo/$image:latest"
docker push "$repo/$image:latest"
fi
echo "registry_uri=$repo/$image" >> "$GITHUB_OUTPUT"

30
.github/workflows/scripts/get-image-tags.rb vendored Executable file
View File

@ -0,0 +1,30 @@
#!/usr/bin/env ruby
require 'json'
require_relative 'lib'
def main
git_repo = ARGV[0]
git_ref_name = ARGV[1]
git_ref_type = ARGV[2]
git_default_branch = ARGV[3]
# log to stderr so that stdout only contains the full tags
$stderr.puts "'#{git_repo}', '#{git_ref_name}', '#{git_ref_type}', '#{git_default_branch}'"
tags =
get_image_tags(
git_repo: git_repo,
git_ref_name: git_ref_name,
git_ref_type: git_ref_type,
git_default_branch: git_default_branch,
package: JSON.parse(File.read('package.json')),
).to_a.join(',')
# log to stderr so that stdout only contains the full tags
$stderr.puts tags
puts tags
end
main()

55
.github/workflows/scripts/lib.rb vendored Normal file
View File

@ -0,0 +1,55 @@
require 'set'
# @param git_repo [String]
# @param git_ref_name [String]
# @param git_ref_type [String]
# @param git_default_branch [String]
# @return [Set[String]]
def get_image_tags(
git_repo: nil,
git_ref_name: nil,
git_ref_type: nil,
git_default_branch: nil,
package: nil
)
container_repo = "ghcr.io/#{git_repo.downcase}"
versions = Set[]
if git_ref_type == 'branch'
# add safe branch name
versions.add(git_ref_name.downcase.gsub(/[^a-z0-9._\n]+/, '-'))
elsif git_ref_type == 'tag'
# add version tag
versions.add(package['version'])
# TODO: check that this is actually latest
parsed = parse_semver(package['version'])
if parsed.pre == nil
versions.add(parsed.major)
versions.add("#{parsed.major}.#{parsed.minor}")
versions.add("#{parsed.major}.#{parsed.minor}.#{parsed.patch}")
end
# TODO: if the tag was made on a non-default branch, we still tag with default branch
versions.add(git_default_branch)
end
# TODO: if `tag`, check that this is actually latest
if git_ref_name == git_default_branch or git_ref_type == 'tag'
# Use Docker `latest` tag convention, only tagging `latest` on default branch.
versions.add('latest')
end
return versions.map! { |v| "#{container_repo}/bot:#{v}" }
end
Semver = Struct.new('Semver', :major, :minor, :patch, :pre, :build)
# @param version [String]
# @return [Semver]
def parse_semver(version)
# Ruby extracts regex named groups to local vars (but only if the regex is inlined).
/^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?:-(?<pre>[0-9A-Za-z\-.]+))?(?:\+(?<build>[0-9A-Za-z\-]+))?$/ =~
version
Semver.new(major.to_i, minor.to_i, patch.to_i, pre, build)
end

View File

@ -0,0 +1,112 @@
require 'test/unit'
require 'json'
require 'set'
require_relative '../lib'
class TestGetImageTags < Test::Unit::TestCase
def test_simple_branch
assert_equal(
Set['ghcr.io/virginity-bot/virginity-bot/bot:feat-foo-bar'],
get_image_tags(
git_repo: 'Virginity-Bot/virginity-bot',
git_ref_name: 'feat/foo-bar',
git_ref_type: 'branch',
git_default_branch: 'master',
package: JSON.parse('{"version": "1.0.0"}'),
),
)
assert_equal(
Set[
'ghcr.io/virginity-bot/virginity.bot/bot:latest',
'ghcr.io/virginity-bot/virginity.bot/bot:master'
],
get_image_tags(
git_repo: 'Virginity-Bot/virginity.bot',
git_ref_name: 'master',
git_ref_type: 'branch',
git_default_branch: 'master',
package: JSON.parse('{"version": "1.0.0"}'),
),
)
end
def test_simple_tag
assert_equal(
Set[
'ghcr.io/virginity-bot/virginity.bot/bot:latest',
'ghcr.io/virginity-bot/virginity.bot/bot:master',
'ghcr.io/virginity-bot/virginity.bot/bot:1.0.0',
'ghcr.io/virginity-bot/virginity.bot/bot:1.0',
'ghcr.io/virginity-bot/virginity.bot/bot:1'
],
get_image_tags(
git_repo: 'Virginity-Bot/virginity.bot',
git_ref_name: '1.0.0',
git_ref_type: 'tag',
git_default_branch: 'master',
package: JSON.parse('{"version": "1.0.0"}'),
),
)
end
def test_pre_tag
assert_equal(
Set[
'ghcr.io/virginity-bot/virginity.bot/bot:latest',
'ghcr.io/virginity-bot/virginity.bot/bot:master',
'ghcr.io/virginity-bot/virginity.bot/bot:1.0.0-pre'
],
get_image_tags(
git_repo: 'Virginity-Bot/virginity.bot',
git_ref_name: '1.0.0',
git_ref_type: 'tag',
git_default_branch: 'master',
package: JSON.parse('{"version": "1.0.0-pre"}'),
),
)
end
def test_unsafe_branch_name
assert_equal(
Set['ghcr.io/virginity-bot/virginity.bot/bot:feat-foo-bar'],
get_image_tags(
git_repo: 'Virginity-Bot/virginity.bot',
git_ref_name: 'feat/Foo---bar',
git_ref_type: 'branch',
git_default_branch: 'master',
package: JSON.parse('{"version": "1.0.0"}'),
),
)
end
end
class TestParseSemver < Test::Unit::TestCase
def test_parse_basic
parsed = parse_semver('1.2.3')
assert_equal(1, parsed.major)
assert_equal(2, parsed.minor)
assert_equal(3, parsed.patch)
assert_equal(nil, parsed.pre)
assert_equal(nil, parsed.build)
end
def test_parse_pre
parsed = parse_semver('1.2.3-p.re')
assert_equal(1, parsed.major)
assert_equal(2, parsed.minor)
assert_equal(3, parsed.patch)
assert_equal('p.re', parsed.pre)
assert_equal(nil, parsed.build)
end
def test_parse_full_semver
parsed = parse_semver('1.2.3-p.re+build')
assert_equal(1, parsed.major)
assert_equal(2, parsed.minor)
assert_equal(3, parsed.patch)
assert_equal('p.re', parsed.pre)
assert_equal('build', parsed.build)
end
end