Zdalne odpalanie Bitbucket Pipeline

Bitbucket Pipelines
Bitbucket, poza tym, że jest serwisem na którym możemy trzymać swoje repozytoria GIT, pozwala nam także na odpalanie Pipeline'ów.
Podobną funckjonalność oferują inne popularne hostingi GITa, takie jak Github (Actions) czy Gitlab (Pipelines). Poza tym, mamy możliwość korzystania z dedykowanych aplikacji zewnętrznych, aby osiągnąć ten sam cel - możemy tutaj wymienić między innymi takie narzędzia jak Circle CI, Travis czy Jenkins.
Uruchomianie Pipeline'ów ma na celu automatyzacje pewnych czynności, takich jak uruchamianie testów, sprawdzanie jakości kodu czy deployment. W skrócie, te akcje, nazywamy CI / CD i stanowią one już pewnego rodzaju standard przy budowaniu nowoczesnych aplikacji. Więcej na ten temat napiszemy w innym poście.
Na tą chwilę, ważne żebyś wiedział, każdy dostawca CI / CD, jak to w życiu bywa, ma zarówno pewne zalety jak i wady. Bitbucket nie jest tutaj wyjątkiem. Dzisiaj postaramy się rozwiązać jeden z problemów, którym jest zdalne odpalanie pipelinów.
Po co nam zdalny dostęp do Pipelines?
Zazwyczaj, konkretny Pipeline, czyli konkretne akcje, wykonywane są po wykryciu zdefiniowanej czynności w repozytorium GIT. Najczęściej jest to Pull Request do repozytorium lub wykrycie zmian w kodzie, na konkretnym branchu.
W innych przypadkach, możemy również ustawić odpalanie Pipeline'a w określonym czasie, cyklicznie. Może być to przydatne przy odpalaniu testów End-to-End, aby sprawdzać, czy nie ma regresji i kluczowe funkcjonalności naszej aplikacji działają jak należy.
Zdalny dostęp może się jednak przydać, gdy chcemy aby Pipeline wystartował w momencie wystąpienia jakichś zmian poza naszym repozytorium GIT. Konkretny use case na który ostatnio natrafilem:
- Wykorzystując frontendowe frameworki takie jak Gatsby czy Next.js, budujemy aplikację frontendową, składa się ona ze statycznych plików HTML
- Statyczne pliki budowane są na podstawie zawartości zewnętrznego CMSa (np Wordpress, Strapi)
- Ktoś wprowadzi zmiany w panelu administracyjnym, doda nowy wpis, edytuje lub usunie już istniejący
- Aplikacja frontendowa powinna się przebudować, aby wygenerować dane zgodne z aktualną zawartością znajdującą się w CMSie.
Dzięki temu, w automatyczny sposób możemy zautomatyzować proces deploymentu mega szybkiej aplikacji, która nie będzie potrzebowała łączenia się z zewnętrznymi serwerami w celu pobierania danych do wyświetlenia. Jest to fajna opcja na prowadzenie niewielkiego bloga czy rzadko aktualizowanej strony, którą jednak chcemy zarządzać z poziomu panelu.
W takim właśnie przypadku, chcielibyśmy poinformować Bitbucket'a o tym, że doszło do jakichś zmian wymagających odpalenia Pipeline'a - w tym przypadku odpowiadającego za przebudowanie aplikacji i deployment.
Ograniczenia i możliwości Bitbucketa
Niektóre aplikacje umożliwiają stworzenie webhooka, którego wystarczy podpiąć w odpowiednie miejsce i aplikacja frontowa może się nam przebudować. Przykładem jest tutaj Strapi CMS + Vercel, w którym możemy sobie wygenerować klucz i podpiąć go w CMSie pod odpowiednie czynności, takie jak dodanie nowego wpisu. Dzięki temu zabiegowi, konkretne zmiany będą automatycznie odpalały budowanie się aplikacji. Super fajne!

Bitbucket Pipeline nie ma możliwości, aby w ultra prosty sposób odpalić dowolny Pipeline. Nie możemy 'wyklikać' sobie linku, pod który po prostu strzelimy i konkretny Pipeline się odpali.. Znaczy, uderzać pod pewien endpoint musimy, ale nie jest to aż takie proste.
Dodatkowo, Bitbucket Pipelines zachowują się domyślnie w sposób, który w tym przypadku stwarza dodatkowe problemy. Otóż jeżeli konkretny Pipeline jest już uruchomiony i trwa, to kolejny, tak samo zdefiniowany Pipeline, automatycznie przechodzi w stan pauzy i możemy go uruchomić ponownie tylko ręcznie. My natomiast chcemy, aby nowy Pipeline powodował zatrzymanie wcześniejszego i rozpoczynał swoją pracę. Niestety, musimy ten przypadek również obsłużyć ręcznie.
Na szczęście, Bitbucket oferuje dość szerokie możliwości za pośrednictwem dedykowanego API. Możemy dzięki temu komunikować się z naszymi Pipeline'ami (i nie tylko) z zewnątrz.
Tak więc.. do dzieła!
Strapi CMS + Bitbucket Pipeline
W naszym konkretnym przypadku, będziemy bazowali na Strapi CMS - który odpowiada za dostarczanie treści do aplikacji frontowej. Po dokonaniu zmian w panelu CMSa, odpalimy konkretny Pipeline Dodatkowo, chcemy zastopować poprzedni, jeżeli nadal trwa.
No ale po kolei:
- Gdy nasze repozytorium na Bitbucket jest prywatne, musimy wygenerować sobie hasło aplikacji - aby móc autoryzować nasze strzały do API. Jeżeli jest publiczne, pomiń ten krok.
- W aplikacji backendowej (Strapi), definiujemy podstawową konfiguracje, uwzględniając między innymi headery czy właśnie dane dostępowe. Wykorzystamy w tym celu bibliteke
Axios
:
baseUrl = "https://api.bitbucket.org/2.0/repositories/<workspace>/<repository>/"
// Default axios settings for BitBucket connections
export const BitBucketClient = axios.create({
baseURL: baseUrl,
timeout: 2000,
headers: {
"Content-Type": "application/json",
},
auth: {
username: "username",
password: "bitbucketAppPassword",
},
})
baseUrl
- jest głównym adresem, pod którym będziemy sie komunikować z Bitbucket'em. Najprościej wejść w konkretne repozytorium w przeglądarce i w URLu pojawi się nam podobny zapis, z którego będziemy mogli przekleić <workspace>
oraz <repository>
.
3. Przygotujmy metodę, która pozwoli nam uderzać do Bitbucket, informując o konieczności ręcznego odpalenia Pipeline'a:
export const invokePipeline = async (): Promise<void> => {
await BitBucketClient.post(`/pipelines/`, {
target: {
ref_type: "branch",
type: "pipeline_ref_target",
ref_name: "production", //Name of target branch
},
})
}
Na dobrą sprawę, tyle nam wystarczy, żeby zdalnie odpalić Pipeline. Teraz wystarczy tylko w odpowiednim miejscu wywołać funkcje invokePipeline
. W przypadku Strapi CMS, jest to plik lifecycles.ts
, w którym mamy dostęp do metod cykli życia kolekcji danych.
4. Wywołanie funkcji invokePipeline
// lifecycles.ts
export default {
afterCreate(event: { params: { data: Record<string, unknown> } }) {
invokePipeline()
},
afterUpdate(event: { params: { data: Record<string, unknown> } }) {
invokePipeline()
},
afterDelete(event: { params: { data: Record<string, unknown> } }) {
invokePipeline()
},
}
W ten sposób, nasz Pipeline, będzie się odpalał za każdym razem gdy dodamy, zaktualizujemy lub usuniemy jakąś stronę kolekcji.
W przypadku innych CMSów, trzeba po prostu znaleźć odpowiednie miejsce gdzie można się wpiąć.
Pamiętajmy jednak, że dobrze jest zamknąć trwający, wcześniejszy Pipeline - jeżeli ktoś przykładowo w krótkim czasie doda i edytuje wpis. Potrzebujemy do tego następujace funkcje
5. Najpierw musimy pobrać ostatnie Pipeline'y i sprawdzić, czy któryś z nich jest aktywny. Jeżeli tak, to zwracamy jego UUID - który jest unikalny identyfikatorem, potrzebnym, żeby w następnym kroku powiedzieć który Pipeline chcemy zatrzymać
const getActivePipelineUUID = async (): Promise<string | undefined> => {
//Get pipelines by newest
const res = await BitBucketClient.get(`/pipelines/?sort=-created_on`)
if ([201, 200].indexOf(res.status) === -1) return
//Only values of pipelines
const pipelines = res.data.values
if (pipelines && pipelines.length) {
//Find pipeline data of our branch ("production")
const latestBranchPipeline = pipelines.find((p: any) => p.target?.ref_name === "production")
if (!latestBranchPipeline) return
const triggeredManually = latestBranchPipeline.trigger.type === "pipeline_trigger_manual"
const isInProgress = ["pipeline_state_in_progress", "pipeline_state_pending"].includes(
latestBranchPipeline.state.type,
)
return triggeredManually && isInProgress ? latestBranchPipeline.uuid : undefined
}
}
6. No i jeszcze potrzebujemy metodę stopującą pipeline
const stopPreviousPipeline = async (uuid: string): Promise<number> => {
const res = await BitBucketClient.post(`/pipelines/${uuid}/stopPipeline`)
return res.status
}
7. Teraz możemy nasze pomocnicze metody sprawdzające, użyć w metodzie głównej invokePipeline
export const invokePipeline = async (): Promise<void> => {
const activePipelineUUID = await getActivePipelineUUID()
if (activePipelineUUID) await stopPreviousPipeline(activePipelineUUID)
const res = await BitBucketClient.post(`/pipelines/`, {
target: {
ref_type: "branch",
type: "pipeline_ref_target",
ref_name: bitBucketBranch,
},
})
}
To tyle. Po wprowadzeniu tych zmian, za każdym razem gdy zmienimy coś w naszym źródle danych - CMSie, odpowiedni Pipeline odpali się powodując przebudowanie aplikacji.
Oczywiście dodatkowo polecam zabezpieczyć tą funkcjonalność o sprawdzenie, czy jesteśmy na wersji produkcyjnej - przez chociażby wykorzystanie zmiennych środowiskowych.
Bitbucket API daje nam dużo więcej możliwości, możemy między innymi odpalać konkretne commity, czyścić cache i wiele więcej. Po szczegóły zapraszam do oficjalnej dokumentacji API https://developer.atlassian.com/cloud/bitbucket/rest/api-group-pipelines/#api-group-pipelines
Komentarze użytkowników