Chiến lược phân nhánh tốt nhất khi thực hiện tích hợp liên tục?


100

Chiến lược phân nhánh tốt nhất để sử dụng khi bạn muốn thực hiện tích hợp liên tục là gì?

  1. Phát hành nhánh: phát triển trên thân cây, giữ một nhánh cho mỗi lần phát hành.
  2. Phân nhánh tính năng: phát triển từng tính năng trong một nhánh riêng biệt, chỉ hợp nhất một lần ổn định.

Sử dụng cả hai chiến lược này cùng nhau có hợp lý không? Như trong, bạn phân nhánh cho mỗi bản phát hành nhưng bạn cũng phân nhánh cho các tính năng lớn? Một trong những chiến lược này có kết hợp tốt hơn với tích hợp liên tục không? Việc sử dụng tích hợp liên tục có thậm chí có ý nghĩa khi sử dụng một đường trục không ổn định?


2
Lưu ý bên lề: một số người cho rằng ngay cả khi các tính năng mới được đưa vào, mọi thứ phải luôn ổn định. Mặt khác, điều đó có thể hơi duy tâm.
Keith Pinson

Câu trả lời:


21

Tôi thấy chủ đề thực sự thú vị vì tôi chủ yếu dựa vào các nhánh trong công việc hàng ngày của mình.

  • Tôi nhớ Mark Shuttleworth đã đề xuất một mô hình về việc giữ nguyên nhánh chính trong khi vượt ra khỏi CI thông thường. Tôi đã đăng về nó ở đây .
  • Vì tôi đã quen với Cruise Control, tôi cũng viết blog về các nhánh nhiệm vụ và CI ở đây . Đó là hướng dẫn từng bước giải thích cách thực hiện với Plastic SCM .
  • Cuối cùng, tôi thấy một số chủ đề về CI (và có khả năng nói về phân nhánh) trong cuốn sách của Duvall về CI cũng rất thú vị .

Hy vọng bạn tìm thấy các liên kết thú vị.


Chúng tôi đã thêm hỗ trợ cho Bamboo để thực hiện phân nhánh mỗi tác vụ codicesoftware.blogspot.com/2012/02/… và có vẻ như phiên bản mới nhất của họ sẽ thực hiện điều đó nguyên bản với một số điều khiển phiên bản, bao gồm cả dvcs.
pablo

20

Câu trả lời phụ thuộc vào quy mô nhóm của bạn và chất lượng kiểm soát nguồn của bạn và khả năng hợp nhất các bộ thay đổi phức tạp một cách chính xác. Ví dụ, trong kiểm soát toàn bộ nguồn chi nhánh như CVS hoặc SVN hợp nhất có thể khó khăn và bạn có thể tốt hơn với mô hình đầu tiên, trong khi nếu sử dụng hệ thống phức tạp hơn như IBM ClearCase và với quy mô nhóm lớn hơn, bạn có thể tốt hơn với mô hình thứ hai mô hình hoặc sự kết hợp của cả hai.

Cá nhân tôi sẽ tách riêng mô hình nhánh tính năng, trong đó mỗi tính năng chính được phát triển trên một nhánh riêng biệt, với các nhánh con nhiệm vụ cho mỗi thay đổi do nhà phát triển cá nhân thực hiện. Khi các tính năng ổn định, chúng sẽ được hợp nhất với thân cây, bạn sẽ giữ ổn định hợp lý và vượt qua tất cả các bài kiểm tra hồi quy mọi lúc. Khi bạn gần kết thúc chu kỳ phát hành của mình và tất cả các nhánh tính năng hợp nhất, bạn ổn định và phân nhánh nhánh hệ thống phát hành mà trên đó bạn chỉ thực hiện sửa lỗi ổn định và các cổng hỗ trợ cần thiết, trong khi thân cây được sử dụng để phát triển bản phát hành tiếp theo và bạn lại rẽ nhánh cho các nhánh tính năng mới. Và như thế.

Theo cách này, thân cây luôn chứa mã mới nhất, nhưng bạn quản lý để giữ cho nó ổn định một cách hợp lý, tạo các nhãn (thẻ) ổn định về những thay đổi lớn và hợp nhất tính năng, các nhánh tính năng phát triển với tốc độ nhanh với sự tích hợp liên tục và các nhánh phụ nhiệm vụ riêng lẻ có thể thường xuyên được làm mới từ nhánh tính năng để đồng bộ hóa mọi người làm việc trên cùng một tính năng, đồng thời không ảnh hưởng đến các nhóm khác đang làm việc trên các tính năng khác nhau.

Đồng thời, bạn có trong lịch sử một tập hợp các chi nhánh phát hành, nơi bạn có thể cung cấp các bản báo cáo, hỗ trợ và sửa lỗi cho khách hàng của mình, những người vì bất kỳ lý do gì vẫn sử dụng các phiên bản trước của sản phẩm hoặc thậm chí chỉ là phiên bản mới nhất được phát hành. Giống như thân cây, bạn không thiết lập tích hợp liên tục trên các nhánh phát hành, chúng được tích hợp cẩn thận khi vượt qua tất cả các bài kiểm tra hồi quy và kiểm soát chất lượng phát hành khác.

Nếu vì lý do nào đó mà hai tính năng cùng phụ thuộc và cần thay đổi lẫn nhau, bạn có thể cân nhắc phát triển cả hai tính năng trên cùng một nhánh tính năng hoặc yêu cầu các tính năng thường xuyên hợp nhất các phần ổn định của mã vào trung kế và sau đó làm mới các thay đổi từ thân cây để trao đổi mã giữa các nhánh thân cây. Hoặc nếu bạn cần tách biệt hai tính năng đó với những tính năng khác, bạn có thể tạo một nhánh chung mà bạn phân nhánh các nhánh tính năng đó và bạn có thể sử dụng để trao đổi mã giữa các tính năng.

Mô hình trên không có nhiều ý nghĩa với các nhóm dưới 50 nhà phát triển và hệ thống kiểm soát nguồn không có chi nhánh thưa thớt và khả năng hợp nhất thích hợp như CVS hoặc SVN, điều này sẽ khiến toàn bộ mô hình này trở thành cơn ác mộng để thiết lập, quản lý và tích hợp.


5
Tôi không chắc liệu tôi có đồng ý rằng những gì bạn mô tả không có ý nghĩa đối với các nhóm dưới 50 nhà phát triển hay không. Tôi có thể thấy lợi ích cho các đội nhỏ hơn nhiều. +1
Aardvark

2
Tất nhiên, có những lợi ích cho các đội ở bất kỳ quy mô nào. Câu hỏi là ở quy mô nhóm nào thì lợi ích lớn hơn chi phí liên quan đến một quy trình nặng nhọc.
Jiri Klouda

Điều này tương tự với mô hình GitFlow và / hoặc GitHubFlow. Tôi không nghĩ rằng những mô hình này tạo điều kiện cho Tích hợp liên tục (CI). Theo tôi, Phát triển dựa trên thân cây là một cải tiến đáng kể trên các mô hình này.
Yani

Bạn có thể thấy rằng nhận xét này thực sự đặt trước ngày phát hành ban đầu của git flow. Không hoàn toàn chắc chắn ý bạn khi nói "tốt hơn". Tôi đã hỗ trợ các nhóm gồm 1, 5, 25, 150, 1.000 và 20.000 nhà phát triển làm việc trên các dự án được tích hợp ở một mức độ nào đó. Các yêu cầu khác nhau và "tốt hơn" là rất nhiều thuật ngữ tương đối. Bạn có cần backport mã bao giờ không? Các bản sửa lỗi bảo mật? Nếu không, thì cuộc sống của bạn thật đơn giản. SaaS là ​​kết quả trực tiếp của các hạn chế do phát triển dựa trên thân cây. Các cờ đặc trưng cũng phức tạp như các nhánh đặc trưng. Ngoại trừ việc bạn chỉ phát hiện ra từ khách hàng khi một hoán vị của họ bị phá vỡ.
Jiri Klouda

9

Cá nhân tôi thấy việc có một thân cây ổn định và phân nhánh tính năng sẽ gọn gàng hơn nhiều. Bằng cách đó, những người thử nghiệm và những người tương tự có thể ở trên một "phiên bản" duy nhất và cập nhật từ thân cây để kiểm tra bất kỳ tính năng nào có mã hoàn chỉnh.

Ngoài ra, nếu nhiều nhà phát triển đang làm việc trên các tính năng khác nhau, tất cả họ đều có thể có các nhánh riêng biệt của riêng mình, sau đó hợp nhất vào trung kế khi họ hoàn thành và gửi một tính năng để được kiểm tra mà người kiểm tra không cần phải chuyển sang nhiều nhánh để kiểm tra các tính năng khác nhau.

Như một phần thưởng bổ sung, có một số cấp độ kiểm tra tích hợp tự động đi kèm.


Ngoài ra, bạn có còn phân nhánh và gắn thẻ cho mỗi bản phát hành chính không? Hay chỉ gắn thẻ?
KingNestor

1
Nó hoạt động tốt với CI miễn là các nhánh tính năng được hợp nhất vào thân với một số kỷ luật để không có các bản dựng bị hỏng. Tôi phân nhánh và gắn thẻ cho mỗi bản phát hành sản xuất sẽ chỉ được sử dụng để sửa lỗi. Điều đó có thể được hợp nhất vào thân cây ổn định ngay lập tức.
Adnan

@king Tôi sẽ nói rằng điều đó có thể phụ thuộc vào những gì bạn gọi là bản phát hành chính, nhưng trong cả hai trường hợp, bạn có thể gắn thẻ và phân nhánh sau khi bạn cần (dựa trên thẻ :))
ví dụ:

5

Tôi nghĩ một trong hai chiến lược có thể được sử dụng với sự phát triển liên tục miễn là bạn nhớ một trong những nguyên tắc chính mà mỗi nhà phát triển cam kết với đường trục / chính hàng ngày.

http://martinfowler.com/articles/continuousIntegration.html#EveryoneCommitsToTheMainlineEveryDay

BIÊN TẬP

Tôi đã đọc cuốn sách này trên CI và các tác giả gợi ý rằng phân nhánh theo phát hành là chiến lược phân nhánh ưa thích của họ. Tôi phải đồng ý. Việc phân nhánh theo tính năng không có ý nghĩa gì đối với tôi khi sử dụng CI.

Tôi sẽ thử và giải thích tại sao tôi lại nghĩ theo cách này. Giả sử ba nhà phát triển mỗi người chọn một chi nhánh để làm việc trên một tính năng. Mỗi tính năng sẽ mất vài ngày hoặc vài tuần để hoàn thành. Để đảm bảo nhóm liên tục tích hợp, họ phải cam kết với nhánh chính ít nhất một lần mỗi ngày. Ngay sau khi họ bắt đầu làm điều này, họ sẽ mất đi lợi ích của việc tạo một nhánh tính năng. Những thay đổi của họ không còn tách biệt với tất cả những thay đổi của nhà phát triển khác. Đó là trường hợp, tại sao phải tạo ra các nhánh tính năng ngay từ đầu?

Sử dụng phân nhánh theo bản phát hành yêu cầu ít hợp nhất giữa các chi nhánh (luôn luôn là một điều tốt), đảm bảo rằng tất cả các thay đổi được tích hợp càng sớm càng tốt và (nếu được thực hiện đúng) đảm bảo cơ sở mã của bạn luôn sẵn sàng để phát hành. Mặt trái của việc phân nhánh theo bản phát hành là bạn phải cẩn thận hơn đáng kể với các thay đổi. Ví dụ: Tái cấu trúc lớn phải được thực hiện từng bước và nếu bạn đã tích hợp một tính năng mới mà bạn không muốn trong bản phát hành tiếp theo thì nó phải được ẩn đi bằng một số loại cơ chế chuyển đổi tính năng .

CHỈNH SỬA KHÁC

Có nhiều hơn một ý kiến ​​về chủ đề này. Đây là một bài đăng trên blog được phân nhánh tính năng chuyên nghiệp với CI

http://jamesmckay.net/2011/07/why-does-martin-fowler-not-und hieu-feature-braught/


thú vị, không thể tìm thấy bài đăng này nữa.
Jirong Hu,

5

Các nhánh phát hành rất hữu ích và thậm chí là hoàn toàn bắt buộc nếu bạn cần duy trì một số phiên bản ứng dụng của mình.

Các nhánh tính năng cũng rất thuận tiện, đáng chú ý là nếu một nhà phát triển cần làm việc với một thay đổi lớn, trong khi những người khác vẫn phát hành phiên bản mới.

Vì vậy, với tôi sử dụng cả hai cơ chế là một chiến lược rất tốt.

Liên kết thú vị từ Sách của SVN .


4

Gần đây tôi đã thích mô hình này khi sử dụng git. Mặc dù câu hỏi của bạn được gắn thẻ "svn", bạn vẫn có thể sử dụng nó.

Tích hợp liên tục ở một mức độ nào đó có thể xảy ra trong nhánh "phát triển" (hoặc bất cứ điều gì bạn gọi) trong mô hình này, mặc dù việc có các nhánh tính năng chạy lâu dài cho các bản phát hành trong tương lai sẽ không khiến nó quá cứng nhắc khi xem xét mọi thay đổi xảy ra với mã ở đâu đó. Câu hỏi vẫn còn, liệu bạn có thực sự muốn điều đó hay không. Martin Fowler thì có.


2

Tích hợp liên tục không nên là bất kỳ loại yếu tố nào trong việc xác định chiến lược phân nhánh của bạn. Cách tiếp cận phân nhánh của bạn nên được lựa chọn dựa trên nhóm của bạn, hệ thống đang được phát triển và các công cụ có sẵn cho bạn.

Có nói rằng ...

  • không có lý do gì CI không thể được sử dụng trong cả hai cách tiếp cận mà bạn mô tả
  • những cách tiếp cận đó hoạt động khá tốt khi kết hợp
  • cả hai đều không hoạt động "tốt" hơn cái kia
  • CI hoàn toàn có ý nghĩa với một đường trục không ổn định

Tất cả điều này đã được trả lời trong câu hỏi thứ tư trên trang mà bạn lấy sơ đồ từ: http://blogs.collab.net/subversion/2007/11/branching-strat/


2

Miễn là bạn hiểu các nguyên tắc, bạn luôn có thể phát minh lại các phương pháp hay nhất. Nếu bạn không hiểu các nguyên tắc, các phương pháp hay nhất sẽ đưa bạn đi xa trước khi sụp đổ do một số yêu cầu bên ngoài mâu thuẫn.

Để có phần giới thiệu tốt nhất về Mô hình dòng chính, hãy đọc phần này: https://web.archive.org/web/20120304070315/http://oreilly.com/catalog/practicalperforce/chapter/ch07.pdf

Đọc liên kết. Khi bạn đã nắm được những kiến ​​thức cơ bản, hãy đọc bài viết sau của Henrik Kniberg đáng kính. Nó sẽ giúp bạn liên hệ Mô hình Mainline với sự tích hợp liên tục.

http://www.infoq.com/articles/agile-version-control


O'Reilly chương không còn truy cập
Jason S

1

Khi chúng tôi thành lập nhóm của mình, chúng tôi đã kế thừa chiến lược dựa trên bản phát hành từ nhà cung cấp ban đầu đã phát triển hệ thống mà chúng tôi sắp phụ trách. Nó hoạt động cho đến thời điểm khách hàng của chúng tôi yêu cầu rằng một số tính năng đã phát triển không được đưa vào bản phát hành (fyi ~ 250k dòng mã, ~ 2500 tệp, Scrum với XP SDLC).

Sau đó, chúng tôi bắt đầu xem xét các chi nhánh dựa trên tính năng. Điều này cũng hoạt động trong một thời gian - như 2 tháng cho đến thời điểm chúng tôi nhận ra rằng quá trình kiểm tra hồi quy của chúng tôi sẽ mất hơn 2 tuần, kết hợp với sự không chắc chắn về những gì sẽ được phát hành đã tạo ra một sự bất tiện lớn.

"Cái đinh trong quan tài" cuối cùng của các chiến lược SC thuần túy đến khi chúng tôi quyết định rằng chúng tôi nên có 1. thân cây ổn định và 2. Sản xuất phải chứa ST, UAT và BINARIES đã được kiểm tra hồi quy (không chỉ nguồn - hãy nghĩ CC.)

Điều này khiến chúng tôi đề ra một chiến lược kết hợp giữa chiến lược SC dựa trên tính năng và phát hành.

Vậy là chúng ta có một cái hòm. Mỗi sprint, chúng tôi phân nhánh sprint (đối với những người không nhanh nhẹn - sprint chỉ là một nỗ lực phát triển trong hộp thời gian với sản lượng thay đổi dựa trên độ phức tạp.) Từ nhánh sprint, chúng tôi tạo ra các nhánh đặc trưng và bắt đầu phát triển song song trong đó. Sau khi các tính năng đã hoàn tất và hệ thống đã được kiểm tra, đồng thời chúng tôi nhận được ý định triển khai chúng, chúng sẽ được hợp nhất với nhánh chạy nước rút - một số có thể trôi nổi trong một số lần chạy nước rút, thường là những chặng phức tạp hơn. Khi sprint gần kết thúc và các tính năng đã hoàn tất ... chúng tôi "đổi tên" nhánh sprint thành "regression" (điều này cho phép CruiseControl nhận nó mà không cần cấu hình lại) và sau đó kiểm tra hồi quy / tích hợp bắt đầu trên cc-build TAI. Khi tất cả những việc đó được hoàn thành, nó sẽ được đưa vào sản xuất.

Nói tóm lại, các nhánh dựa trên tính năng được sử dụng để phát triển, kiểm tra hệ thống và chức năng UAT. Nhánh sprint (thực sự là nhánh phát hành) được sử dụng để kết hợp có chọn lọc các tính năng theo yêu cầu và kiểm tra tích hợp.

Bây giờ đây là một câu hỏi cho cộng đồng - rõ ràng chúng tôi đang gặp khó khăn khi thực hiện tích hợp liên tục vì thực tế là sự phát triển xảy ra trên nhiều nhánh và chi phí cấu hình lại của CruiseControl. Ai đó có thể gợi ý và lời khuyên?


Tôi không nhất thiết đồng ý với kết luận, nhưng cảm ơn vì đã thảo luận về quy trình của bạn. Không có giải pháp chung cho tất cả.
RaoulRubin

0

Theo cách tôi thấy, bạn muốn có một tập hợp giới hạn các nhánh để bạn có thể tập trung. Vì bạn muốn kiểm tra, chỉ số chất lượng mã và nhiều thứ thú vị để chạy với các bản dựng, nên việc có quá nhiều báo cáo có thể khiến bạn bỏ lỡ thông tin.

Chi nhánh khi nào và cái gì, thường phụ thuộc vào quy mô của nhóm và quy mô của các tính năng đang được phát triển. Tôi không nghĩ rằng có một quy tắc vàng. Đảm bảo rằng bạn sử dụng một chiến lược mà bạn có thể nhận được phản hồi sớm / thường xuyên và bao gồm cả chất lượng liên quan ngay từ đầu của các tính năng. Bit chất lượng, có nghĩa là khi bạn đang tự động hóa khi nhóm phát triển, nếu bạn phân nhánh cho một bộ tính năng lớn mà nhóm đang xây dựng, bạn cũng phải có chất lượng tham gia vào nhóm.

ps Bạn lấy những tài liệu tham khảo cách tiếp cận đó ở đâu? - không cảm thấy rằng những biểu đồ đó đại diện cho tất cả các tùy chọn

Cập nhật 1: Mở rộng lý do tại sao tôi nói đó không phải là quy tắc vàng. Về cơ bản đối với các đội tương đối nhỏ, tôi thấy tốt nhất là sử dụng phương pháp kết hợp. Các nhánh tính năng được tạo nếu nó dài và một phần của nhóm sẽ tiếp tục thêm các tính năng nhỏ hơn.


Nó cũng có một số nữa. Nhưng tôi cảm thấy như Phân nhánh tính năng và Phân nhánh phát hành là 2 cách phổ biến nhất.
KingNestor

0

Dave Farley , một tác giả của Phân phối liên tục , gọi Phát triển dựa trên thân cây (TBD) là nền tảng của Tích hợp liên tục (CI) và Phân phối liên tục (CD). Anh ta nói:

Bất kỳ hình thức phân nhánh nào đều trái nghĩa với Tích hợp liên tục.

Anh ấy cũng nói,

Tính năng Phân nhánh rất hay từ quan điểm của một nhà phát triển cá nhân nhưng lại không tối ưu từ quan điểm của một nhóm. Tất cả chúng tôi đều muốn có thể bỏ qua những gì mọi người khác đang làm và tiếp tục công việc của mình. Thật không may, mã không phải như vậy. Ngay cả trong các cơ sở mã được kiểm chứng rất tốt với các mối quan tâm phân tách đẹp mắt và các thành phần được ghép nối lỏng lẻo một cách tuyệt vời, một số thay đổi sẽ ảnh hưởng đến các phần khác của hệ thống.

Phát triển dựa trên thân cây (TBD) là thực hành tích hợp các thay đổi mã vào đường trục chính (hay còn gọi là đường chính, đường chính) ít nhất một lần mỗi ngày - tốt nhất là nhiều lần mỗi ngày. Tích hợp liên tục (CI) là một hoạt động tương tự ngoại trừ việc nó cũng liên quan đến việc xác minh các thay đổi mã bằng cách sử dụng các bài kiểm tra tự động. Chiến lược phân nhánh tốt nhất cho việc này là làm việc trực tiếp ngoài đường trục và thực hiện đánh giá mã thông qua Lập trình cặp . Nếu vì lý do nào đó mà bạn không thể ghép hoặc bạn chỉ thực sự muốn phân nhánh, hãy đảm bảo rằng các nhánh của bạn chỉ tồn tại trong thời gian ngắn (dưới một ngày).

Tôi làm việc trên Trunk, "bậc thầy" trong các repo GIT của tôi. Tôi cam kết làm chủ cục bộ và đẩy ngay lập tức, khi tôi được nối mạng, đến kho lưu trữ chính trung tâm của tôi nơi CI chạy. Đó là nó!

Đối với các tính năng lớn (tức là những tính năng mất nhiều thời gian hơn một ngày), hãy cố gắng chia chúng thành các phần logic nhỏ có thể được tích hợp vào trung kế mà không làm hỏng phần mềm. Bạn cũng có thể sử dụng các kỹ thuật như gắn cờ đối tượngphân nhánh bằng cách trừu tượng hóa , cho phép bạn triển khai công việc chưa hoàn thiện mà không ảnh hưởng đến người dùng cuối.

Tôi sử dụng nhánh bằng cách trừu tượng hóa, giải phóng bóng tối và đôi khi là cờ đặc trưng. Những gì tôi nhận được đổi lại là phản hồi nhanh chóng, dứt khoát (ít nhất là đối với chất lượng thử nghiệm của tôi).


Dave Farley và Jez Humble chỉ đơn giản là sai trong lập trường của họ về phân nhánh. Lý do là nó mã hóa giả định quan trọng "bạn sẽ không bao giờ phải thao tác mã ở cấp độ tính năng và nếu, nó là hoạt động tốn kém thì cũng được" và họ dựa trên đánh giá của mình trên một giả định khác "việc hợp nhất là quá đắt với tự động gần như không thể hợp nhất ở quy mô lớn ". Nếu hai giả định đó không đúng, nếu bạn sống trong thế giới mà việc hợp nhất là rẻ, nhưng cần thao tác mã ở cấp tính năng cho các cổng trở lại và các bản sửa lỗi bảo mật, thì các tuyên bố của chúng sẽ bị phá vỡ. Đó là một trường hợp hiếm hoi tho.
Jiri Klouda

Một số công ty cũng cần chuyển các tính năng sang các bản phát hành trong tương lai, sau khi các tính năng đó gặp trở ngại trong việc triển khai và đang giữ bản phát hành. Đôi khi có một tùy chọn để nguyên mã, giống như trong các sản phẩm SaaS, nhưng nếu mã được phát hành cho khách hàng, nó có thể không phải là một tùy chọn vì đối thủ cạnh tranh có thể phân tích mã. Ngày nay, rất nhiều mã không được biên dịch và ngay cả khi có, các cờ định nghĩa / tính năng trong mã có cùng mức độ phức tạp như các nhánh.
Jiri Klouda

-3

Tôi nghĩ rằng các công cụ bạn sử dụng là một yếu tố lớn ở đây.

  • Nếu bạn đang sử dụng subversion, hãy gắn bó với tùy chọn 1 và giải phóng khỏi các nhánh.
  • Nếu bạn đang sử dụng GIT, tùy chọn 2 sẽ hoạt động tốt cho bạn.

2
Có thể dễ dàng phân nhánh tính năng với bất kỳ SCM nào
hdost
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.