admin管理员组

文章数量:1646251

景观指数计算

I won’t elaborate here on how important and crucial for any software development-oriented team the continuous integration (CI) practise is.I’m pretty sure we can all agree on how CI tools support our day to day effectiveness. How they might save dozens of hours spent on non-essential tasks. Yet, it’s common to present CI tools as a hassle; slow, bulky, and unreliable pipelines bloated with chaotic events instead of fast, maintainable feedback loop configured to support both product quality and team flexibility.

我不会在这里详细说明持续集成 (CI)的实践对任何面向软件开发的团队的重要性和重要性。我敢肯定,我们都可以同意CI工具如何支持我们的日常有效性。 他们如何节省数十小时用于非必要任务的时间。 然而,摆弄CI工具很麻烦。 缓慢,庞大且不可靠的管道充斥着混乱的事件,而不是配置为支持产品质量和团队灵活性的快速,可维护的反馈回路。

As the title implies, our CI process was far from optimal. We learned what “slow and chaotic” means the hard way. Below, you will find an overview of each issue that slowed us down, with full explanation of what the solution was (including code and external links), as well as honest results measured by minutes.

顾名思义,我们的CI流程远非最佳。 我们了解了“缓慢而混乱”意味着艰难的方式。 下面,您将概览每个使我们放慢脚步的问题,并全面说明解决方案是什么(包括代码和外部链接),以及按分钟计算的真实结果

In this article, you will find discussion surrounding architecture, flavour agnostic unit testing, Gradle usage as well as keeping your logs and artefacts deployment in order. Additionally, at the end of the article, several tips and tricks beyond optimisation will be included. It’s not a step-by-step tutorial. We gathered results that work for us, and you have to think them through. If those solutions make sense to you then, and only then, apply them to your environment.

在本文中,您将找到有关体系结构,与风味无关的单元测试,Gradle用法以及使日志和人工制品部署保持秩序的讨论。 此外,在本文结尾处,将包括优化以外的一些技巧和窍门。 这不是分步教程。 我们收集了对我们有用的结果, 必须仔细考虑它们。 如果这些解决方案对您有意义,则只有在那时,才能将其应用于您的环境。

景观 (The landscape)

In order to fully understand why we provided a particular optimisation it is crucial to understand how our landscape looked at the time.

为了充分理解我们为什么提供特定的优化,了解当时的情况至关重要。

There is git flow approach in place, which usually means multiple feature branches exist at the same time in a remote repository. There is at least one pull request per story. Each pull request needs to go through an integration process meaning the newest commit in a pull request triggers a fresh CI build. That’s being done in order to ensure the newest change won’t introduce any flaws. Yep, automation and unit test suites test each software incrementation. Software Engineers in Test (SET) writes automation tests as “a part of“ the feature in some cases.

有适当的git flow方法 ,这通常意味着远程存储库中同时存在多个功能分支。 每个故事至少有一个请求请求 。 每个请求请求都需要经过一个集成过程,这意味着请求请求中的最新提交会触发新的CI构建。 这样做是为了确保最新的更改不会带来任何缺陷。 是的,自动化和单元测试套件测试每个软件增量 。 在某些情况下,测试中的软件工程师(SET)将自动化测试写为该功能的“一部分”。

We are supporting multiple modules as a part of our architecture. Let’s assume it is a clean-ish architecture with domain, data and app layers packed into separate modules. Each of the modules has its own unit tests suite — between dozens to few hundreds of them per module. We have to support multiple flavours and they differ greatly. Each flavour has a separate set of automation and unit test, although most of them are shared.

我们正在支持多个模块,这是我们架构的一部分。 我们假设它是一个干净的体系结构 ,将domaindataapp层打包到单独的模块中。 每个模块都有自己的单元测试套件-每个模块介于几十到几百个之间。 我们必须支持多种口味 ,它们之间存在很大差异。 每种口味都有一套单独的自动化和单元测试,尽管它们大多数是共享的。

When it comes to infrastructure, there is a separate Bitrise workflow for every build type. Also, a separate one for each of: feature development, automation efforts, release (tags) activities and after merging feature to the develop. Seeing how many distinct configs we have, there is a need to run multiple builds every day. We can’t and won’t have “infinite” amount of concurrent jobs, so time devoted to each build is very important to us. It’s also important because we value sh*t done the right way.

对于基础架构,每种构建类型都有单独的Bitrise工作流。 另外,针对以下各项分别使用一项: 功能开发, 自动化工作, 发布 ( 标签 )活动以及将功能合并到开发中之后。 看到我们有多少个不同的配置,有必要每天运行多个构建。 我们不能也不会拥有“无限”数量的并发工作,因此花在每个构建上的时间对我们来说非常重要。 这也很重要,因为我们重视以正确的方式做事

The basic measurement that will prove effectiveness here is build time — both entire build time or a particular step time (such as unit tests step or deploy step).

这里将证明有效性的基本度量是构建时间 - 整个构建时间或特定步骤时间(例如单元测试步骤或部署步骤)。

改进之处 (Improvements)

单元测试 (Unit testing)

The most commonly used feedback loop is unit tests suite, in particular if you’re supporting multiple flavours for Android app and you want to be sure that none of the changes would break any of the flavours. Unit tests are supposed to be a fast and reliable feedback loop, which can be automated at the CI level. So, we used docs and tutorials to set them up for all of the flavours. After few changes to CI, we ended up with 30 minutes long unit test step for 3 flavours. Yes, you read it properly: 30 minutes for 3 flavours.

最常用的反馈循环是单元测试套件,特别是如果您支持Android应用程序的多种口味,并且您想确保所有更改都不会破坏任何口味。 单元测试应该是一个快速可靠的反馈回路,可以在CI级别将其自动化。 因此,我们使用了文档和教程来针对所有口味进行设置。 在对CI进行少量更改之后,我们完成了30分钟的3种口味的单元测试步骤。 是的,您没有看错:30分钟有3种口味。

Ok, let’s fix that.

好吧,让我们解决这个问题。

After a little bit of research it occurred to us that we used two separate steps for unit tests. Android unit test step for Bitrise was running app module unit tests. Gradle Unit Test step was just running .gradlew test task.

经过一番研究,我们发现我们对单元测试使用了两个单独的步骤。 Bitrise的Android单元测试步骤正在运行应用程序模块单元测试。 Gradle Unit Test步骤仅在运行.gradlew测试任务。

Total time for each step in minutes.
每一步的总时间(以分钟为单位)。

What’s wrong with gradle unit test step in our case? According to the Gradle documentation:

在我们的案例中,gradle单元测试步骤有什么问题? 根据Gradle文档:

Source: https://docs.gradle/current/userguide/command_line_interface.html
资料来源: https : //docs.gradle/current/userguide/command_line_interface.html

In simple terms, ./gradlew test triggers dozens of different test tasks from every module. In our case, it triggered both debug and release related tests for every subproject (module). That’s too much redundancy; consider the final result of ./gradlew test command:

简而言之,。/ gradlew测试从每个模块触发许多不同的测试 任务 。 在我们的例子中,它触发了每个子项目(模块)的 调试发布相关测试。 那太冗余了; 考虑./gradlew测试命令的最终结果:

(amount of flavours) x (amount of supported envs) x (amount of modules)

(风味量)x(支持的envs)x(模块数量)

But the amount of tasks triggered is not all we can improve here. I already mentioned we have several modules. Since it’s cleanish architecture, it consists of app, domain, data and api modules. It’s easy to see that some of those modules are flavour agnostic — domain, data and api layers can and should be treated as libraries. Those are external dependencies that could be used via any JVM compatible code. Do we need to run those tests separately for each flavour? Of course we don’t! Where does it lead us?

但是触发的任务数量并不是我们可以改善的全部。 我已经提到过我们有几个模块。 由于它是干净的架构,因此包含appdomaindataapi模块。 显而易见,其中某些模块与风味无关 ,可以并且应该将域,数据和api层视为库。 这些是外部依赖关系可以通过任何与JVM兼容的代码使用。 我们是否需要针对每种口味分别运行这些测试? 当然不! 它把我们引向何方?

风味不可知单元测试 (Flavour agnostic unit tests)

Split flavour dependent and flavour agnostic unit tests. Gain greater control over how your application is tested. Use Gradle Unit Test step in your Bitrise.yml to run targeted flavour agnostic unit tests, like this:

拆分风味依赖风味不可知单元测试。 更好地控制应用程序的测试方式。 使用Bitrise.yml中的 Gradle Unit Test步骤来运行目标风味不可知的单元测试,如下所示:

_UnitTests_Flavour_Agnostic_Modules:
    steps:
      - gradle-unit-test@1.0.5:
          inputs:
            - gradlew_file_path: "./gradlew"
            - unit_test_task: ":data:testDebugUnitTest"
            - gradle_file: "./build.gradle"
          title: "Data module unit testing"
      - gradle-unit-test@1.0.5:
          inputs:
            - gradlew_file_path: "./gradlew"
            - unit_test_task: ":domain:test"
            - gradle_file: "./build.gradle"
          title: "Domain unit testing"
      - gradle-unit-test@1.0.5:
          inputs:
            - gradlew_file_path: "./gradlew"
            - unit_test_task: ":feature1:test"
            - gradle_file: "./build.gradle"
          title: "Feature1 unit testing"

Using unit_test_task attribute enables you to configure a particular task to be run. Basically, any gradle task. You can obviously chain gradle commands, but I want granularity here. Additionally, the usage of title attribute keeps build logs in order and enables you to track each step separately.

使用unit_test_task属性使您可以配置要运行的特定任务。 基本上,任何gradle任务。 您显然可以链接gradle命令,但我想在此进行粒度描述 。 此外,使用title属性可以使构建日志井然有序,并使您能够分别跟踪每个步骤。

The result of applying the above mentioned recommendations. Cleaning up resources gave us unit tests result in seconds instead of minutes.
应用上述建议的结果。 清理资源使我们在几秒钟而不是几分钟内即可获得单元测试结果。

依赖于风味的单元测试 (Flavour dependent unit tests)

The second recommendation relates to Android unit test step for Bitrise and how flavour dependent unit tests are managed. In most cases, I would recommend you to run only what you need. But I came to conclusion that ‘run only what you need’ could be counterintuitive in our case.

第二项建议涉及Bitrise的Android单元测试步骤,以及与风味相关的单元测试的管理方式。 在大多数情况下,我建议您仅运行所需的内容。 但是我得出的结论是,在我们的案例中,“只运行您需要的内容”可能违反直觉。

It’s really easy to break one of the flavours by introducing changes to only one of them. That’s why we ended up with running unit tests for every flavour in every build. In addition, the above mentioned set of flavour agnostic tests is triggered. What does it mean when it comes to Bitrise CI setup?

通过仅对其中一种进行更改就可以轻松打破其中一种口味。 这就是为什么我们最终针对每种构建中的每种口味运行单元测试的原因。 另外,触发了上述风味不可知测试组。 说到Bitrise CI设置是什么意思?

_UnitTestsPerFlavour:
    steps:
      - android-unit-test:
          inputs:
            - module: app
            - variant: ${FLAVOUR_NAME}UatDebug
          title: "Flavour unit tests"
    before_run:
      - _UnitTests_Flavour_Agnostic_Modules

The above snippet runs unit tests for the app module for a particular flavour injected as an environment variable and a particular build variant. So, if CI builds only one flavour at time, this snippet is supposed to be triggered three times, once for each flavour. If all of the flavours are built simultaneously, then each flavour should run its own unit tests in order to avoid redundancy and save a few minutes from build. Notice that, before _UnitTestsPerFlavour step, UnitTests_Flavour_Agnostic_Modules step is triggered. It runs flavour agnostic tests, so domain, data and feature modules unit tests. Either way, all unit tests are always validated.

上面的代码段针对作为环境变量和特定构建变体注入的特定风味为app模块运行了单元测试。 因此,如果CI一次只能建立一种口味,则该片段应触发3次,每种口味均触发一次。 如果所有风味都同时构建,则每种风味应运行自己的单元测试,以避免冗余并从构建中节省几分钟。 请注意,在_UnitTestsPerFlavour步骤之前已触发UnitTests_Flavour_Agnostic_Modules步骤。 它运行与风味无关的测试,因此对数据和功能模块进行单元测试。 无论哪种方式,所有单元测试都始终经过验证。

Alternatively to the above setup, you can use the following setup to hardcode which flavour’s unit tests should be run:

除了上述设置之外,您还可以使用以下设置来硬编码应运行哪种风味的单元测试:

_TargetedUnitTestForAllFlavours:
    steps:
      - gradle-unit-test@1.0.5:
          inputs:
            - gradlew_file_path: "./gradlew"
            - unit_test_task: ":app:testFlavourAUatDebugUnitTest"
            - gradle_file: "./build.gradle"
          title: "FlavourA App unit testing"
      - gradle-unit-test@1.0.5:
          inputs:
            - gradlew_file_path: "./gradlew"
            - unit_test_task: ":app:testFlavourBUatDebugUnitTest"
            - gradle_file: "./build.gradle"
          title: "FlavourB App unit testing"
      - gradle-unit-test@1.0.5:
          inputs:
            - gradlew_file_path: "./gradlew"
            - unit_test_task: ":app:testFlavourCUatDebugUnitTest"
            - gradle_file: "./build.gradle"
          title: "FlavourC App unit testing"
    before_run:
      - _UnitTests_Flavour_Agnostic_Modules

That way we’re all covered, no matter if we’re building all the flavours, or just one. Remember, the lesson here is flavour dependent and agnostic unit tests should be triggered once. There is no redundancy, but there is full coverage. Every software increment is safe.

这样一来,无论我们是制作所有口味,还是只制作一种口味,我们都可以覆盖。 请记住,这里的课程是与风味有关的,不可知论的单元测试应该触发一次。 没有冗余,但是有完整的覆盖范围。 每个软件增量都是安全的

结果 (Results)

We started with around 30 minutes per build.

每次构建大约需要30分钟

Total time for unit testing then.
然后进行单元测试的总时间。

And finished up with the below results when running one flavour.Down to ~5/6 minutes per build.

并在运行一种口味时得出以下结果。 每次构建时间约5/6分钟。

Total time for unit testing now.
现在进行单元测试的总时间。

And also down to ~3 minutes per build when running all the flavours at once, which means each flavour is responsible for its own unit tests finally. Yes, that’s a separate config in order to optimise build time even further.

一次运行所有版本时, 每个构建版本的时间缩短到大约3分钟 ,这意味着每种版本最终都要负责自己的单元测试。 是的,这是一个单独的配置,目的是进一步优化构建时间。

Total time when running all of the flavours. Build time for those unit tests per one flavour.
运行所有调味料的总时间。 每一种口味的单元测试的构建时间。

文物部署 (Artefacts Deployment)

Review your Deploy to Bitrise.io step. According to the documentation [1] [2] [3] for the following steps test reports are deployed automatically:

查看您对Bitrise.io的部署步骤。 根据文档[1] [2] [3]的以下步骤,将自动部署测试报告:

  • Xcode Test for iOS

    适用于iOS的Xcode测试
  • Android Unit Test

    Android单元测试
  • iOS Device Testing

    iOS设备测试
  • Virtual Device Testing for Android

    Android虚拟设备测试

As noted in the documentation, by default Android unit and UI tests are deployed to Bitrise directory and are provided via the Test reports tab. They are easily accessible — but the question is — are they really necessary?

如文档中所述,默认情况下,Android单元和UI测试将部署到Bitrise目录,并通过“ 测试报告”选项卡提供。 它们很容易访问-但问题是-它们真的必要吗?

We have robust unit tests. They fail rarely in CI because the entire team writes and runs them frequently. On the other hand, it’s easy to check Bitrise for which logs failed.

我们有强大的单元测试。 它们在CI中很少失败,因为整个团队经常编写和运行它们。 另一方面,检查Bitrise日志失败很容易。

决定部署什么 (Deciding what to deploy)

We already changed from Android unit test step for Bitrise to Gradle Unit Test step which does not deploy unit tests reports automatically. And we want it that way. What about the rest of the artefacts? For automation builds we’ve decided not to deploy any APKs. They are not needed.

我们已经从Bitrise的Android单元测试步骤更改为Gradle单元测试步骤,该步骤不会自动部署单元测试报告。 我们想要那样。 其余的文物呢? 对于自动化构建,我们决定不部署任何APK。 不需要它们。

We also already know that Virtual Device Testing for Android step deploys UI tests results into the Test Reports directory. We decided that for all of the builds we are going to move or remove Deploy to Bitrise.io step completely as an experiment. Also, Deploy to Bitrise.io step is always triggered before unit tests but after APK creation. That way, only application (uatRelease APK for example) and the UI tests report are deployed.

我们也已经知道, “ Android虚拟设备测试”步骤将UI测试结果部署到“ 测试报告”目录中。 我们决定,对于所有构建,我们将作为实验完全将D eploy移动或删除到Bitrise.io 。 同样, 部署到Bitrise.io的步骤始终在单元测试之前但在APK创建之后触发。 这样,仅部署应用程序(例如uatRelease APK)和UI测试报告。

Initially deploy to Bitrise.io step took from 2.1 to 3.2 minutes.

最初部署到Bitrise.io的 步骤耗时2.1至3.2分钟

Initially 3.2 min was total time per this step.
最初,此步骤的总时间为3.2分钟。

After the changes it’s 0 minutes for some builds. It is ~8 seconds for most of them.

更改后,某些构建为0分钟。 对于大多数人来说,这大约是8秒。

That’s how quick it could be!
那就这么快!
Oh yeah! Source: https://knowyourmeme/photos/988454-we-did-it-reddit
哦耶! 资料来源: https : //knowyourmeme/photos/988454-we-did-it-reddit

自动化工作流程 (Automation workflow)

One of the low hanging fruits was to change what is being done as a part of a particular workflow, since they all have different goals.As I mentioned, we have feature, automation, develop and release workflow.In our case, initially, all of the mentioned workflows had basically the same setup. Why is this wrong? Because, as we said, workflows simply have different responsibilities.

一项低落的成果是改变特定工作流程中要完成的工作,因为它们都有不同的目标,正如我提到的那样,我们具有功能自动化开发发布工作流程。提到的工作流程中的设置基本相同。 为什么会这样呢? 因为,正如我们所说,工作流仅具有不同的职责

了解工作流程的差异 (Understanding the differences in workflows)

I have already mentioned the automation workflow. It’s because it is special compared to other workflows. The only responsibility automation workflow has is to support Software Engineers in Test in writing and securing automation test suite. That simple conclusion means we can trim several steps from it; in our case, APK and other artefacts creation and deployment. We were also able to get rid of custom scripts we had there for the release app or “runtime” resources optimisations steps and beyond.

我已经提到了自动化工作流程。 这是因为与其他工作流程相比,它是特殊的。 自动化工作流程的唯一职责是支持测试中的软件工程师编写和保护自动化测试套件。 这个简单的结论意味着我们可以从中减去几个步骤; 在我们的案例中,APK和其他人工制品的创建和部署。 我们还可以摆脱发布应用程序或“运行时”资源优化步骤以及之后的自定义脚本。

结果 (Results)

By doing this, the automation build is a fast feedback loop for the SETs.It takes around 10 minutes less than other builds.I believe it’s a huge win for the SETs team.

通过这样做, 自动化构建是SET的快速反馈回路。 与其他版本相比,它只需花费约10分钟的时间。 我相信这对SET团队是一个巨大的胜利。

调查工具配置 (Investigating tools configuration)

Here is a quick and simple story as an example. Our builds produced uatDebug and uatRelease APKs. UAT stands for ‘user acceptance testing’ and it’s also a name of one of our environment s— environment with almost production setup but more over development data — and simply used for testing purposes. So, producing those two build sounds about right, doesn’t it? I started asking questions anyway. We were sure we need uatRelease for testing purposes. It makes sense since testing production ready app (release) using development data (uat) is one of the best practices. But why do we need uatDebug then?

这里以一个简单的故事为例。 我们的构建生成了uatDebuguatRelease APK。 UAT代表“ 用户接受测试 ”,它也是我们的环境之一的名称-环境几乎具有生产设置,但超出了开发数据范围-仅用于测试目的。 因此,产生这两个构建声音大约是正确的,不是吗? 我还是开始问问题。 我们确定我们需要uatRelease进行测试。 这是有道理的,因为使用开发数据( uat )测试可用于生产的应用程序( 版本 )是最佳实践之一。 但是,为什么我们需要uatDebug

整理未使用的资源 (Trimming unused resources)

The sole reason was a misconfiguration of the Charles proxy tool, which led testers to not being able to use proxy tools while testing uatRelease build variant. Famous network_security_config file had been added to the project but it wasn’t working, since the build variant has to be debuggable. The quick fix was to add android:debuggable attribute to all uat builds. And since we’re not testing uat builds using any public channels — it’s secure enough.

唯一的原因是Charles代理工具的配置错误,这导致测试人员在测试uatRelease构建变体时无法使用代理工具。 著名的network_security_config 文件已添加到项目中,但由于构建变体必须是可调试的 ,因此无法正常工作 快速解决方案是将android:debuggable属性添加到所有uat构建中。 而且,由于我们不使用任何公共渠道来测试uat版本,因此足够安全。

结果 (Results)

A simple configuration fix to the existing toolset brought an 8 minutes time reduction to each build and fixed SETs headache.

对现有工具集的简单配置修复为每个构建节省了8分钟的时间,并解决了SET令人头疼的问题。

所有数字加在一起 (All numbers together)

In summary

综上所述

  • Unit tests time down from 30 minutes to 3~6 minutes. Depends on build type.

    单元测试时间从30分钟缩短到3〜6分钟。 取决于构建类型。

  • Automation build cut off by another 10 minutes through removing a few unnecessary steps.

    通过删除一些不必要的步骤,自动化构建又缩短了10分钟

  • Artefacts deployment reduced from 2.1~3.2 minutes into 8 seconds!

    人工制品的部署时间 2.1〜3.2 分钟减少到8秒!

  • Fix to Charles configs gave us another 8 minutes — due uatDebug build removal.

    修复了Charles的配置,又给了我们8分钟的时间 -删除了uatDebug构建。

We were able to shorten builds by between 48 minutes and 34 minutes per each build. That was a huge win and relief as you can imagine!

我们能够将每次构建缩短48分钟到34分钟之间的时间 。 可以想象,那是一次巨大的胜利和轻松!

We obviously made some rookie mistakes. But the most important part is to learn from them. We were able to adapt quickly and we’re providing other small improvements since then. It can’t happen on a daily basis because we also need to deliver business value to our clients — but with an appropriate plan in place, I’m sure you can do even more.

我们显然犯了一些菜鸟错误。 但是最重​​要的部分是向他们学习。 从那时起,我们就能够快速适应并提供其他小改进。 它不可能每天都发生,因为我们还需要为客户提供业务价值 -但是有了适当的计划,我相信您可以做得更多。

优化之外的提示和技巧 (Tips and tricks beyond optimisations)

Bitrise and its plugins’ documentation is quite limited. You will need to deep dive into the plugins code if you want to understand the platform fully. Plugins code is mostly open source — you can find links inside plugin documentation. In particular, review the main.go file if you’re looking for attributes and parameters which could customise the build.

Bitrise及其插件的文档非常有限。 如果您想完全了解平台,则需要深入研究插件代码。 插件代码大部分是开源的-您可以在插件文档中找到链接。 特别是,如果要查找可以自定义构建的属性和参数,请查看main.go文件。

Use Bitrise CLI in your terminal in order to test configuration locally. It will save you a lot of time.

在终端中使用Bitrise CLI以便在本地测试配置。 这样可以节省您很多时间。

Have as granular CI steps as possible. Use title attribute extensively. Greater readability — greater control over time. Solid foundations are the first step for future optimisation.

尽可能执行精细的CI步骤。 广泛使用title属性。 更高的可读性-更好地控制时间。 坚实的基础是未来优化的第一步。

Do what we haven’t done yet — introduce tools to measure build metrics automatically.

做我们尚未完成的工作-引入自动测量构建指标的工具。

Leverage version control since Bitrise is similar to infrastructure as a code.

由于Bitrise 基础架构 类似 ,因此可以利用版本控制。

That’s a separate story but in Tigerspike, we optimised APK size by 13% during our internal hackathon day. You should be aware of best practises for Android app configuration. Get rid or optimise resources, configuration and APK size. These kinds of things are also impacting your build time: git pull, compilation and build time, tests, deploy time — these are some of many examples.

那是一个单独的故事,但是在Tigerspike中,我们在内部黑客马拉松日将APK大小优化了13% 。 您应该了解Android应用配置的最佳做法。 摆脱或优化资源,配置和APK大小。 这些事情也会影响您的构建时间:git pull,编译和构建时间,测试,部署时间-这些是许多示例。

Listen. Observe. Experiment. Formulate a plan and adopt only what’s needed for your team. Good luck!

听。 观察一下。 实验。 制定计划并仅采纳团队需要的内容。 祝好运!

Thanks! Source: http://123emoji/donald-duck-stickers-2-9606/
谢谢! 资料来源: http : //123emoji/donald-duck-stickers-2-9606/

I hope you like this piece. As you can see I love a fast feedback loop — if you have any objections, comments or questions — please drop a comment or DM message.

我希望你喜欢这块。 如您所见,我喜欢快速反馈循环-如果您有任何异议,评论或问题,请发表评论或DM消息。

The article showcases what we have done for one of our projects in Tigerspike. We’re hiring — please mention my name! ;)

本文展示了我们为Tigerspike中的一个项目所做的工作 。 我们正在招聘 -请提及我的名字! ;)

If you want to reach me out, I’m based in Wrocław, Poland.I’m also visiting London from time to time.Here is my LinkedIn and Twitter.

如果您想联系我,我位于波兰的弗罗茨瓦夫,我还不时访问伦敦,这是我的LinkedIn和Twitter 。

翻译自: https://proandroiddev/be-effective-with-bitrise-ci-for-android-lessons-i-learned-the-hard-way-5a85e45a33dc

景观指数计算

本文标签: 景观指数