二分查找拐点的一个问题

    题目: 一个先升序再降序的数组, 二分查找拐点位置

    package main
    
    import (
    	"testing"
    	"time"
    )
    
    func binsearch(nums []int) (idx int, count int) {
    	s := 0
    	e := len(nums) - 1
    	var m int
    	for s < e {
    		m = (s + e + 1) / 2
    		count++
    		if nums[m] > nums[m-1] {
    			s = m
    		} else {
    			e = m - 1
    		}
    	}
    
    	return s, count
    }
    
    func binsearch2(nums []int) (idx int, count int) {
    	s := 0
    	e := len(nums) - 1
    	var ee int = e
    	var m int = 0
    	for s < e {
    		m = (s + e + 1) / 2
    		count++
    		if nums[m] > nums[m-1] {
    			count++
    			if m == ee || nums[m] > nums[m+1] {
    				return m, count
    			}
    			s = m
    		} else {
    			e = m - 1
    		}
    	}
    
    	return s, count
    }
    
    func createTestData(n int) [][]int {
    	testData := make([][]int, n)
    	for i := range testData {
    		sortNums := make([]int, n)
    		for j := 0; j < i; j++ {
    			sortNums[j] = j
    		}
    		for j := i; j < n; j++ {
    			sortNums[j] = i + n - j
    		}
    		testData[i] = sortNums
    	}
    	return testData
    }
    
    func main() {
    	testData := createTestData(1024)
    
    	var s, e int64
    
    	s = time.Now().UnixNano()
    	var totalCount int = 0
    	for i := 0; i < 1000; i++ {
    		for _, nums := range testData {
    			_, count := binsearch(nums)
    			totalCount += count
    		}
    	}
    	e = time.Now().UnixNano()
    
    	println(totalCount)
    	println(e - s)
    
    	s = time.Now().UnixNano()
    	totalCount = 0
    	for i := 0; i < 1000; i++ {
    		for _, nums := range testData {
    			_, count := binsearch2(nums)
    			totalCount += count
    		}
    	}
    
    	e = time.Now().UnixNano()
    	println(totalCount)
    	println(e - s)
    }
    
    func BenchmarkBinSearch(b *testing.B) {
    	testData := createTestData(1024)
    
    	b.ResetTimer()
    	var totalCount int = 0
    	for i := 0; i < b.N; i++ {
    		for _, nums := range testData {
    			_, count := binsearch(nums)
    			totalCount += count
    		}
    	}
    	//println(totalCount)
    }
    
    func BenchmarkBinSearch2(b *testing.B) {
    	testData := createTestData(1024)
    
    	b.ResetTimer()
    	var totalCount int = 0
    	for i := 0; i < b.N; i++ {
    		for _, nums := range testData {
    			_, count := binsearch2(nums)
    			totalCount += count
    		}
    	}
    	//println(totalCount)
    }
    

    方法2跑了更多的分支, 但是时间却比较短. 为什么呢?

    Read More...

    Kotlin Lombok Data

    刚刚发现一个 Kotlin 文件不能访问 lombok.Data 里面的private属性, 而另外一个可以.

    之前遇到过类似的问题, 所以原因我也一下就想到了~ :) 但是到网上搜索了一下解决方法, 却发现解决不了哈哈. 记录一下.

    之前遇到的问题好像是这样的, 一个 Java 方法没办法访问 Kotlin Data Class 里面的属性. 解决方法 G 了一下就找到了, 在官网也有说明, 是编译顺序的问题, 需要先编译 Kotlin 再 Java

    那这次遇到的问题, 恰恰反了过来, 需要先编译 Java, 那就没得搞了.

    前面说一个Kt 能访问 lombok.Data class, 另外一个不能, 是因为他们不在一个 Module

    Read More...

    一起来打牌

  • {{ totalScore(person) }} {{ person.name }} {{ score }}
  • Read More...

    Graphql Java

    写在最前

    我不是写一个 GraphQL 的使用指南, 是在写自己对 GraphQL 的一些使用感受. 我自己是非常喜欢它, 愿意给他打10分, 虽然还有些小瑕疵. 如果你用过 GraphQL 了, 可以看看我们是不是有同感.

    另外多提两句. 1 GraphQL 本身是一个协议, 每种语言都有自己的框架实现, 我自己只用过 Java 的. 2 他是一个执行引擎, 不是 HTTP 框架, 你可以在任何地方使用他们, 当然现在最常见的是提供 HTTP 接口.

    当前开发模式(未使用 GraphQL 前)

    先说 API 协议设计上面.

    请求(Request): Client 给需要的一些参数. 拿取文章列表这个接口举例: 请求中给分页大小 当前是第几页 排序方法(热度/发布时间/打分等).

    响应(Response): 就是按需要给 Client 返回他需要的字段.

    然后简单说一下代码实现上面.

    从数据库,或者是别的服务接口取回需要的数据, 然后按一定的逻辑做一些过滤, 做一些字段的组装, 返回给调用者.

    痛点

    1. 难以做到按需提供 API. 这是 GraphQL 着重宣传的点, 但我的觉得后面两点才是 GraphQL 更爽的地方.

      想像一样, 我们给客户 A 提供了一个文章列表的 API. 后面客户 B 说我们还需要文章作者的昵称. 我们就要在 API Response 里面再添加一个字段, 但这个字段 A 并不需要. 后面客户 C 又说我们需要另外一个字段, 字段可能会越加越多, 而其他人可能并不需要.

      取有些字段可能还会比较费时, 那么给 C 加的字段导致 A 调用时长增加了. 这时候, 我们可能会在 Request 里面增加一个参数, 可以默认不返回某字段, 只有 C 调用的时候才返回这个字段.

      又比如说, 用户 A 需要对每篇文章返回10条评论,C 需要20条.我们又需要一个参数,这些参数基本上都是最外层的,没有规范的命名标准.

      总的来说, 没有一套规范, 这点和 GraphQL 有鲜明的对比.可能每个点都可以用自己的方法解决掉,但 GraphQL 是降维打击

    2. Do Dto Ao Vo 等各层对象转来转去. 增加一个字段, 可能会在每个转换的地方都要修改代码, 非常繁琐.

    3. 飞线代码多, 逻辑分散在各个地方, 特别是多人开发, 项目多次转交后.

      就我自己的实际经验, 来举个例子, 一个新接手的项目. 文章作者下面有标签字段, 包含标签类型和标签名字. 新需求想把一个接口A 里面的作者标签做些过滤, 某种类型的标签就不要返回了.

      AuthorService 生成 Author 以及标签的函数, 被多个其他函数引用, 这些函数又被引用, 最后被多个接口使用. 那对我来说, 完成需求的最简单的方法, 就是在接口 A 最后返回的时候, 把标签过滤一下. 我把这段代码叫做飞线代码. 时间长了, 这种情况多了, 飞线越来越多, 字段的值可能会在多个地方, 在不同的逻辑下被修改. 给 Debug 和代码可读性带来痛苦.

    GraphQL 优势

    减少 dto - ao - vo 转换

    各种 O(Object) 的定义: 阿里巴巴Java开发手册中的DO、DTO、BO、AO、VO、POJO定义

    这些各种 O 的转换, 本意大概是想让代码更清楚, 减少耦合. 但实际上, 给我的体验非常糟糕, 添加一个字段需要在多个 O 之间转来转去. 我接过一个项目, 只是数据库里面多一个字段返回给前端, 需要修改大概10处代码, 就只是在 OOO 之间Get/Set

    GraphQL 里面(特指 Java 框架,其他语言的没有用过), 因为 data mapping 和 DataFetcher 的存在, 实际操作下来, 并不需要这些 OOO 之间的转换, 而且逻辑反而更清楚.

    代码复用

    虽然这里是写了”代码复用”, 但更像一种配置的复用. 我觉得比代码复用更简单, 更清楚, 写起来更简单, 别人看起来也一目了然.

    这个是 GraphqL 的框架本身的优势 (这里也特指 Java 的). 因为他的逻辑在字段和 DataFetcher 的绑定这里, 而不在具体的业务代码里面.

    举下例子吧.

    文章里面有返回作者字段(里面有头像,昵称等字段). 评论(Comment)现在也要加作者字段. 在之前的开发模式下呢, 就是找到评论这块代码, 通过评论里面的 authorId 去调用 AuthorService 代码, 返回到 Comment 对象里. 这里我这里只用了一句话来说明需要做什么, 但实际上, article->comment->author 一层层找下去, 并不是很愉快的事, 这里还只三层而已. 另外还有dto - ao - vo 转换让我非常头疼.

    GraphQL (Java框架) 里面, 只要把 Comment 的 Author 字段绑定到 AuthorFetcher 就好了, 应该就一行代码.

    逻辑清晰

    这个是针对前面提到的飞线代码. 因为 GraphQL 天然的一个字段对应一个 DataFetcher, 所以逻辑再怎么飞也飞不多远. 在 GraphQL 里面做一些逻辑的修改是很愉快的事.

    更自然的并行

    也许吧, 我只是列在这里了, 其实对这一点, 我自己的感受并不深刻. 可能是我们的 QPS 太低, 对响应时间也不太苛刻, 不用并行也无所谓, 感受不到.

    规范化的批量处理

    比如说有好多地方需要使用 imageId 去取 image 的具体信息, 你会把这些地方合并起来批量去取吗? 你可能会很纠结, 因为代码写起来会麻烦一些, 而且充满了回调这样的东西. GraphQL 里面的 DataLoader (java 特有? 不确定) 为你提供了一个规范的批量处理的方式, 而且使用非常自然.

    但他未必能把所有 imameId 收集到一起再批量去取. 请阅读和实践一下 Dispatch 的概念, 还有 dataLoader 那一篇官方文档. 我觉得这是 GraphQL 的一个缺陷, 但已经够好了, 不是吗?

    一些实践经验

    设计好schema

    很想把这句放到文章最前面, 怎么强调都不过分. 要按照天然的数据结构和层次来设计 Schema, 天然的结构是指按 DB 里面的表结构, 以及其他接口返回的数据结构等等. 我觉得这是自然而然的事情, 但还是要说一下, 以免有人会有老一套思维定势, 觉得这样不合理.

    如果你或者前端同学觉得这样不合理, 比如说觉得这样导致一些字段的获取层次太深了, 或者是你认为把一些字段抽取出来放在一个 Struct 里才更”合理”. 我给的建议是, 不要用 GraphQL 了.

    平级和跨级依赖

    这是GraphQL 的一个痛点.

    比如说, 我们有一个 Atuhor Struct, Author 里面有 photoId 这个字段, 我们需要通过这个字段去取头像(Photo)的具体数据. 也就是说, Photo 依赖 PhotoId.

    photoId 是数据库里面一起返回的, 这样没问题. 因为运行到 Photo 绑定的 DataFetcher 的时候, photoId 必然已经存在了, 只要取 source.photoId 就可以.

    但是但是, 如果 photoId 不在 DB 里面一个字段, 而是绑定了一个非默认的 DataFetcher. 这样不能用source.photoId. 可以使用上下文(Context)来传递这个数据, 但问题是运行到 Photo 绑定的 DataFetcher 的时候, 因为两个 DataFetcher 是并行的, 这时photoId 可能还没有取到, Context 传过来的数据可能是一个空值.

    我只能用 Future 这种东西来处理这个问题, 但是代码又绕又丑. 具体做法是在 Context 里面放一个 Future, 然后在 photoId 的 DataFetcher 里面 Complete, 在 Photo 的 DataFetcher 里面 Apply

    我是希望 GraphQL 可以在框架上解决这个问题, 比如说提供类似 source.getPhotoId() 的方法, 他会自动等待 photoId 的 DataFetcher 完成.

    上面是说平级的依赖.

    跨级的依赖是说上层的上层(或者更上层)里面的属性, 不能方便的拿到, 只能走 Contxt 来传递, 不方便, 在 IDE 里面也很难做到跳转.

    尽量使用 DataLoader

    如果有批量处理的需求, 使用 DataLoader

    手动 Dispatch

    TODO

    dataLoader 注册名字规范

    需要定义一个规范, 大家都按同样的规范来取名字. 但依然不够好, 不方便跳转, 我取一个 DataLoader 的时候, 没办法方便的跳转到他的定义. 也是 GraphQL 的一个痛点.

    async

    默认执行策略是”异步执行策略”, 但在 DataFetcher 里面要用 Async 包一下才会真正的异步执行.

    enum

    Schema 里面尽量使用 Enum, 更加语义化, 使用方一眼就能看明白怎么用, 也可以避免一些笔误带来的错误.

    GraphQL-Java 框架的不足

    依赖

    如上面提到的

    无奈使用 Context 传递数据

    如上面提到的

    像 JAVA 框架现在还在开发迭代中, 希望他能在框架层面解决一些问题.

    另外如果有人能提供 IDE 的插件也可以解决一些问题, 比如说 IDE 里面的跳转. 毕竟更好的语义分析能力的也是大家使用 IDE 的一个重要原因.

    Read More...

    Contains Not Working In Elasticsearch

    elastic search painless script 里面的 contains “不生效”, 还好有 Google

    参考资料 https://discuss.elastic.co/t/painless-collection-contains-not-working/178944/2

    上面的链接 实在是太慢了, 摘抄一下

    Because Elasticsearch treats those numbers as Longs by default, you need to make sure that you pass a Long to the contains method. The following should work:

    GET testdatatype_unit_tests/_search
    {
      "size": 100,
      "query": {
        "script": {
          "script": {
            "source": "doc['IntCollection'].values.contains(1L)",
            "lang": "painless"
          }
        }
      }
    }
    
    Read More...

    算法导论7.1-快排描述

    7.1.2

    当数据 A[p..r] 中的元素均相同时, Partition 返回的 q 是什么. 修改 Partition, 使得数据 A[p..r] 中的元素均相同时, 返回 q=(p+r)/2

    返回 r

    我想不到好办法, 只能 i,j 分别从两头想向走. 代码如下:

    def partiton(nums, p, r):
        if len(nums) <= 1:
            return 0
        x = nums[r]
        i, j = p, r-1
        while True:
            if nums[i] > x:
                nums[i], nums[j] = nums[j], nums[i]
                j -= 1
            else:
                i += 1
            if i > j:
                break
    
            if nums[j] < x:
                nums[i], nums[j] = nums[j], nums[i]
                i += 1
            else:
                j -= 1
            if i > j:
                break
    
        nums[i], nums[r] = nums[r], nums[i]
        return i
    
    
    def main():
        nums = [2]*6
        q = partiton(nums, 0, len(nums)-1)
        assert q == 3
    
        nums = [2]*7
        q = partiton(nums, 0, len(nums)-1)
        assert q == 3
    
        for i in range(10):
            nums = [0]*i
            q = partiton(nums, 0, len(nums)-1)
            assert q == i//2
    
        nums = list(range(10))
        q = partiton(nums, 0, len(nums)-1)
        assert q == 9
    
        nums = [1, 2, 3, 4, 6, 7, 8, 9, 5]
        q = partiton(nums, 0, len(nums)-1)
        assert q == 4
    
        import random
        for _ in range(100):
            nums = []
            for _ in range(10):
                nums.append(random.randint(1, 10))
            q = partiton(nums, 0, len(nums)-1)
            assert not nums[:q] or max(nums[:q]) <= nums[q]
            assert not nums[q+1:] or min(nums[q+1:]) >= nums[q]
    
    
    if __name__ == '__main__':
        main()
    

    7.1.4

    反过来写一下

    def partiton(nums, p, r):
        i=p-1
        x = nums[r]
        for j in range(p,r):
            if nums[j]>x:
                nums[i+1],nums[j] =nums[j],nums[i+1]
                i+=1
        nums[i+1],nums[r] =nums[r],nums[i+1]
        return i+1
    
    
    def main():
        import random
        for _ in range(1000):
            nums = []
            for _ in range(10):
                nums.append(random.randint(1,10))
                q = partiton(nums,0,len(nums)-1)
                print(nums,q,nums[:q],nums[q+1:],nums[q])
                assert not nums[:q] or min(nums[:q]) >= nums[q]
                assert not nums[q+1:] or max(nums[q+1:]) <= nums[q]
    
    if __name__ == '__main__':
        main()
    
    Read More...

    如何把一个功能在一行代码里面实现

    学习资料

    几前年看过酷壳的这两篇文章, 当时以为已经掌握了. 现在又想实践一下的时候, 发现完全不知道怎么写了. 所以温习并记录一下.

    步骤(套路)

    可以认为, 如何把一个功能在一行代码里面实现, 是有固定的套路的.

    我还不能马上理解里面的原因原理, 所以自认为是套路, 其实是我自己太弱.

    分下面4个步骤, 看不懂也没关系, 下面有例子具体演示说明

    1. 用递归实现此功能
    2. 将递归函数做为参数传入调用函数(是我写不清楚, 具体看下面例子)
    3. 使用匿名函数替换(是我写不清楚, 具体看下面例子)
    4. 高阶函数(是我写不清楚, 具体看下面例子)

    例子一 求阶乘

    步骤1 递归实现阶乘

    求阶乘用递归还是很自然的, 如下

    def fact(n):
        return 1 if n==0 else fact(n-1)*n
    print(fact(5))
    

    步骤2

    def fact(fun, n):
        return 1 if n==0 else fun(fun,n-1)*n
    
    print(fact(fact,5))
    

    其实很简单, 就是把 fact 这个函数本身传参进去, 这是第三步的基础

    步骤3

    我们先把 fact 写成一个 lambda

    fact = lambda fun,n : 1 if n==0 else fun(fun,n-1)*n
    

    然后把fact(fact,5)里面的 fact 变量直接换成上面这个 ldmbda 就好了, 如下

    fact = lambda fun,n : 1 if n==0 else fun(fun,n-1)*n
    print((lambda fun,n : 1 if n==0 else fun(fun,n-1)*n)((lambda fun,n : 1 if n==0 else fun(fun,n-1)*n),5))
    

    但这样有两个缺点

    1. 大段的代码重复
    2. 我们本来只是传参一个数字, 但现在还要传参一个匿名函数

    这就不太体面了, 需要继续修改下. 这也就是步骤3的意义所在.

    我们再定义一个函数 r 如下

    def r(f,n):
        return f(f,n)
    

    r 函数有个优点: 短. 我们只要把 fact 代入这个短的函数中, 就可以去掉大段的重复代码了. 完成代码如下:

    fact = lambda fun,n : 1 if n==0 else fun(fun,n-1)*n
    r = lambda f,n: f(f,n)
    print(r(fact,5))
    

    把变量替换掉, 就变成一行代码了

    print((lambda f,n: f(f,n))((lambda fun,n : 1 if n==0 else fun(fun,n-1)*n),5))
    

    步骤4

    但是缺点2还是没有解决掉, 步骤4就是解决这个问题的.

    我们引入一个高阶函数 h, 这个函数返回另外一个函数 inner , inner 是一个参数为 int 的函数, 如下

    其实就是把上面的代码复制到了 h() 里面

    def h():
        def inner(n):
            fact = lambda fun,n : 1 if n==0 else fun(fun,n-1)*n
            r = lambda f,n: f(f,n)
            return r(fact,n)
        return inner
    
    print(h()(5))
    

    把变量替换一下

    def h():
        return lambda n:(lambda f,n: f(f,n))((lambda fun,n : 1 if n==0 else fun(fun,n-1)*n),n)
    print(h()(5))
    

    把 h 也替换掉

    print((lambda:lambda n:(lambda f,n: f(f,n))((lambda fun,n : 1 if n==0 else fun(fun,n-1)*n),n))()(5))
    

    或者也可以写成

    print((lambda n:lambda:(lambda f,n: f(f,n))((lambda fun,n : 1 if n==0 else fun(fun,n-1)*n),n))(5)())
    

    把 fun 全部换成 f , 能短一点是一点

    print((lambda:lambda n:(lambda f,n:f(f,n))((lambda f,n:1 if n==0 else f(f,n-1)*n),n))()(5))
    

    补充 2020-01-16T12:24:07+0800

    觉得第四步可以把最外层的 h() 扒掉.

    def h():
        return lambda n:(lambda f,n: f(f,n))((lambda fun,n : 1 if n==0 else fun(fun,n-1)*n),n)
    print(h()(5))
    

    像上面这段代码, 其实没有必要先返回一个函数然后再调用这个函数. 可以直接调用, 如下

    (lambda n:(lambda f,n:f(f,n))((lambda f,n:1 if n==0 else f(f,n-1)*n),n))(5)
    
    Read More...

    Golang Reflect Method Performance

    reflect_test.go

    package main
    
    import "testing"
    
    type A struct {
    	name string
    }
    
    type SetNamer interface {
    	SetName()
    }
    
    func (a *A) SetName() {
    	a.name = "a"
    }
    
    func getA() *A {
    	return &A{}
    }
    func getI() SetNamer {
    	return &A{}
    }
    
    func BenchmarkA(b *testing.B) {
    	a := &A{}
    	b.ResetTimer()
    	for i := 0; i < b.N; i++ {
    		a.SetName()
    	}
    	print(a.name)
    }
    
    func BenchmarkB(b *testing.B) {
    	var s SetNamer = &A{}
    	b.ResetTimer()
    	for i := 0; i < b.N; i++ {
    		s.SetName()
    	}
    }
    
    func BenchmarkC(b *testing.B) {
    	a := getA()
    	b.ResetTimer()
    	for i := 0; i < b.N; i++ {
    		a.SetName()
    	}
    	print(a.name)
    }
    
    func BenchmarkD(b *testing.B) {
    	s := getI()
    	b.ResetTimer()
    	for i := 0; i < b.N; i++ {
    		s.SetName()
    	}
    }
    

    测试结果:

    % go test -bench=. -cpu=1
    agoos: darwin
    goarch: amd64
    BenchmarkA 	aaaaa1000000000	         0.574 ns/op
    BenchmarkB 	537799974	         2.17 ns/op
    aBenchmarkC 	aaaaa1000000000	         0.571 ns/op
    BenchmarkD 	545279128	         2.22 ns/op
    PASS
    ok  	_/private/tmp/1576831712	4.101s
    

    同样一个 value a, 或者它的申明类型是 struct A , 调用 a.SetName 速度要很多

    相比这下, 如果申明类型是 interface SetNamer, 调用 s.SetName 速度要非常多

    Read More...

    Git Flow Practice

    背景

    1. 公司 APP 每个月迭代一次, 比如说每个月 15 号, 发布一个新版本
    2. 当前只有一个分支, 所有代码都合并到这个分支, 比如说都合到 master 分支
    3. 公司的发布系统, 发布之后可以看到每个版本使用的 branch/tag/commit hash 等(这个后面会提到)

    这样会生产一个问题:

    15 号发版之后, 开始开发新功能, 合到 master 上面, 这些代码会到下个月15号发到线上.
    但同时, HotFix 需要马上发到线上. 如果从 master checkout 一个 hotfix 分支出来改代码, 再合回master. master上面会包括还没有测试过的新功能. 那这个hotfix发布之后可能带来问题.

    最初讨论后的规划

    如何在现有的基础(单分支)上解决这个问题

    中间有同学提出目前这样也可以解决上面的问题: 功能开发正常推到 master , 如果有 hotfix, 到发布系统看一下当前的 Commit 或者是 Tag, 拉一个新的分支, fix 之后推上去继续发布.

    但会有一些问题:

    • 每次要做 hotfix, 还要去发布系统查看当前的 tag 或者是 commit
    • 如果两个人同时做 hotfix, 可能后面发布的人同学会把前面那个人的覆盖掉
    • 分支混乱, 每次 hotfix 后推一个新的分支上来?

    会后讨论结果(实践后又有变化):

    决定使用dev 和 master 两个分支

    1. 日常开发推 dev, 15号之后合到 master , 打上 tag
    2. 15号之后的 hotfix , 从 master 拉分支出来, fix之后合回master, 同时 cherry-pick 到 dev
    3. 细节上可能会有问题, 再实践中慢慢摸索

    改动

    增加relese 分支

    代码在真正发布到生产之后, 需要先发布到测试环境测试.
    使用 dev 发布并没有问题, 但如果结合持续集成(CI), 就有些问题了.

    我希望的是, 每次 Merge 之后, 会自动发布, 而不需要我每次到发布系统点点点.
    显然不能把 Dev 分支配置成自动发布, 否则在新的开发周期, 会频繁做一些无用的发布.

    所以需要 release 分支. 发布的时候, 直接从 dev checkout 一份最新代码, 推到远端 release, 自动发布.

    15 号发布之后, 就把当前 release 代码合到 master, 并打上 tag

    保留 master

    如果只是为了回滚, tag 足够了. master 是为了方便的 hotfix. 想像一下没有 master, 每次发布还要查看最新的 tag 是什么, 万一再忘了打 tag 呢?

    目前实践

    总结一下, 实践下来, 保留 2 个主要分支, dev 和 master, 一直存在, 不能删除. 一个临时分支 release.

    以一次新的迭代周期开始为例:

    1. 新的功能开发推到 dev. hotfix 推到 master, 并更新版本号, 并 cherry-pick 到 dev.
    2. 10 号了, 开始预发布了(一般是先到测试环境), 每次发布就把 dev 的代码合到 release, 推到远端(我一般在命令行操作), CI 会自动做发布.
    3. 15 号, 发布到生产. release 代码合到 master, 并打一个 tag. (可以由项目 Owner 或者管理员 手工操作)
    4. 15 号之后, 回到步骤1

    2020-03-30 补充

    有人提到使用 release 发布太麻烦了, 应该 dev 也能发布. 现成改成了 dev 也会发布. 运行良好.

    参考资料:

    Read More...