Przejdź do treści

Dokumentacja dla deweloperów

Ostatnia aktualizacja 26 września 2026.

Wszystko, co Changeloop dla Ciebie publikuje, to zwykły JSON przez HTTPS. Nie ma SDK do zainstalowania, klucza API do rotacji ani kroku logowania: dwa poniższe feedy to anonimowe publiczne odczyty kluczowane identyfikatorem Twojego feedu. Zamień YOUR_PUBLIC_ID na swój w dowolnym przykładzie na tej stronie.

Jedna rzecz, którą warto wiedzieć przed zaczęciem: identyfikator Twojego publicznego feedu znajduje się w samej aplikacji. Zaloguj się, otwórz Ustawienia, a jest tam w sekcji Publiczny feed, tej, na której lądujesz domyślnie, wraz z gotowymi linkami do changelog.json i roadmap.json, linkiem do Twojej hostowanej strony feedu i fragmentem kodu widgetu poniżej, każdy z własnym przyciskiem kopiowania.

Pierwsze kroki

Pięć kroków prowadzi od rejestracji do changeloga na Twojej własnej stronie. Strona "Get started" w aplikacji przeprowadzi Cię przez nie i odhaczy każdy krok, gdy go ukończysz.

  1. Podłącz źródło: repozytorium GitHub, projekt GitLab lub repozytorium Bitbucket.
  2. Wybierz język, w którym pisane są Twoje wpisy.
  3. Opcjonalnie utwórz tagi, aby czytelnicy mogli filtrować według obszaru produktu.
  4. Opublikuj swój pierwszy wpis. Scalone zmiany trafiają jako szkice do skrzynki recenzji: zatwierdź jeden albo włącz automatyczne publikowanie dla tego repozytorium.
  5. Umieść go na swojej stronie: dodaj link do strony hostowanej, wklej widget albo wyświetl feed JSON na własnej stronie.

Twój changelog w około dziesięciu liniach Reacta

Wklej to do komponentu i masz działający changelog. Nie ma nic więcej do dodania.

import { useEffect, useState } from 'react';

const FEED = 'https://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/changelog.json';

export function Changelog() {
  const [entries, setEntries] = useState([]);
  useEffect(() => {
    fetch(FEED).then((r) => r.json()).then((feed) => setEntries(feed.data));
  }, []);
  return <ul>{entries.map((e) => <li key={e.id}><b>{e.title}</b><p>{e.mdContent}</p></li>)}</ul>;
}

mdContent to markdown, który przygotowaliśmy, jako tekst. Jeśli wolisz renderować sformatowany wynik, użyj zamiast tego htmlContent: jest budowany po stronie serwera przez nasz własny sanitizer z ustalonej listy dozwolonych tagów i atrybutów, i jest jedyną wartością w którejkolwiek z tych odpowiedzi przeznaczoną do wstrzyknięcia jako znacznik. Reszta to tekst, a wpisy tworzone z publicznego repozytorium mogą być pod wpływem każdego, kto może tam otworzyć pull request, więc traktuj je odpowiednio.

Feed changeloga

GET/v1/public/YOUR_PUBLIC_ID/changelog.json

Twoje opublikowane wpisy, od najnowszych, z najnowszym identyfikatorem rozstrzygającym remisy przy identycznych znacznikach czasu.

Parametry zapytania

  • repos przyjmuje listę pełnych nazw repozytoriów rozdzielonych przecinkami, np. acme/web,acme/api. Zwracane są tylko wpisy z tych repozytoriów. Pomiń, a otrzymasz wszystkie.
  • limit to liczba wpisów, jakie chcesz na stronę. Domyślnie 20, wszystko powyżej 50 jest ograniczane do 50, a wszystko, czego nie umiemy odczytać jako liczby dodatniej, wraca do 20 zamiast się nie powieść.
  • cursor jest nieprzejrzysty. Weź wartość nextCursor z poprzedniej odpowiedzi i zwróć ją dokładnie taką, jaka jest. Kursor, którego nie umiemy zdekodować, jest traktowany tak, jakby go nie było, więc otrzymujesz z powrotem pierwszą stronę zamiast błędu.

Odpowiedź

{
  "data": [
    {
      "id": "66b0c1f2e4a9d1c3b5a70011",
      "title": "Saved views on the inbox",
      "mdContent": "You can now pin a filter and come back to it.",
      "htmlContent": "<p>You can now pin a filter and come back to it.</p>",
      "repoFullName": "acme/web",
      "category": "feature",
      "tags": ["Inbox"],
      "learnMoreUrl": "https://acme.example/docs/saved-views",
      "publishedAt": "2026-08-06T09:12:44.000Z"
    }
  ],
  "nextCursor": null,
  "tagColors": { "Inbox": "#4f46e5" }
}

Każdy wpis niesie te same dziewięć pól: id, title, mdContent, htmlContent, repoFullName, category, tags, learnMoreUrl i publishedAt. category to feature, fix lub internal i jest null, gdy autor jej nie ustawił; publishedAt to ciąg ISO 8601, a htmlContent jest pustym ciągiem we wpisie, który nigdy nie przeszedł przez redaktora. tags to tablica nazw Twoich własnych obszarów produktu i jest pusta, gdy żadnego nie przypisano, learnMoreUrl jest null, chyba że recenzent go dodał, a kolor każdego tagu pochodzi z mapy tagColors w odpowiedzi, a nie z wpisu, więc tag, który usunąłeś ze swojego słownictwa, po prostu renderuje się bez koloru. nextCursor jest null, gdy dotrzesz do końca.

Nieznany identyfikator feedu odpowiada 404 z {"error":"not_found"}, podobnie jak źle sformatowany. Oba są celowo nierozróżnialne, więc ten endpoint nie może być użyty do sprawdzenia, które identyfikatory istnieją.

Feed roadmapy

GET/v1/public/YOUR_PUBLIC_ID/roadmap.json

Te same trzy kolumny, które Twój zespół już utrzymuje ręcznie.

{
  "columns": [
    { "column": "planned", "items": [], "hasMore": false },
    {
      "column": "building",
      "items": [
        {
          "id": "66b0c1f2e4a9d1c3b5a70042",
          "column": "building",
          "publicTitle": "Slack notifications",
          "publicDescription": "Post each published entry to a channel you pick.",
          "publishedAt": "2026-08-05T16:20:01.000Z"
        }
      ],
      "hasMore": false
    },
    { "column": "shipped", "items": [], "hasMore": false }
  ]
}

columns to tablica, nie obiekt indeksowany nazwą kolumny, a jej kolejność jest częścią kontraktu: planned, potem building, potem shipped. Wszystkie trzy są zawsze obecne, w tym puste, więc nigdy nie musisz odróżniać "takiej kolumny nie ma" od "nic w niej jeszcze nie ma". Renderuj w otrzymanej kolejności, a będziesz zgodny z każdą inną powierzchnią, którą budujemy.

Element ma dokładnie pięć pól: id, column, publicTitle, publicDescription i publishedAt. publicDescription jest zawsze ciągiem i może być puste, nigdy null. Nic o issue, z którego pochodzi element, nie jest tu ujawniane, ani repozytorium, ani numer issue, i jest to celowe, a nie zaniedbanie, które uzupełnimy później.

Ten endpoint nie przyjmuje żadnych parametrów zapytania. Nie ma kursora, limitu ani filtra repozytorium, ponieważ roadmapa to mała tablica kuratorowana przez osobę, a nie log rosnący w nieskończoność. Każda kolumna zwraca do 50 elementów i ustawia hasMore, jeśli było ich więcej. hasMore jest informacyjne: nie ma kursora, by za nim podążyć, więc nie buduj wokół tego paginacji.

publicTitle i publicDescription to zwykły tekst tworzony z tytułów i treści issue, na które w publicznym repozytorium może wpłynąć każdy, kto otworzy issue. Nie mają żadnej gwarancji sanityzacji HTML i nie są wyjątkiem htmlContent. Renderuj je jako tekst.

Osadzalny widget

Jeśli wolisz niczego nie budować, wklej te dwie linie. Widget to custom element renderowany w shadow root, więc ani nie dziedziczy Twoich stylów, ani do nich nie wycieka.

<script src="https://api.changeloop.dev/widget.js" defer></script>
<changelogapp-widget
  data-public-id="YOUR_PUBLIC_ID"
  data-api="https://api.changeloop.dev"></changelogapp-widget>

Oba atrybuty są wymagane. data-public-id to identyfikator Twojego feedu, data-api to pochodzenie, z którego widget pobiera dane. Jeśli brakuje któregokolwiek, element zapisuje błąd w konsoli i nic nie renderuje, co jest pierwszą rzeczą do sprawdzenia, jeśli widzisz puste miejsce tam, gdzie powinien być widget.

Dodaj do elementu data-theme="dark", by wyświetlał się w ciemnej wersji; Twoja strona może to przełączać w trakcie działania. Do głębszej stylizacji widget udostępnia własności CSS (--changelogapp-text, --changelogapp-bg, --changelogapp-accent i inne) oraz nazwy ::part(), które ustawiasz we własnym arkuszu stylów. Aplikacja pokazuje podgląd obu motywów na żywo w Ustawieniach, sekcja Publiczny feed.

Dodaj data-repos, aby pokazać tylko część swoich repozytoriów, na przykład changelog jednego produktu na stronie tego produktu, gdy kilka produktów dzieli jedno konto. Wartość to lista pełnych nazw owner/repo rozdzielonych przecinkami; sama nazwa bez właściciela do niczego nie pasuje i wyświetla pusty kanał bez błędu. Uwzględnianych jest najwyżej dziesięć repozytoriów. Tak zawężony widżet pokazuje tylko zakładki Updates i Feedback, bo roadmapa nie ma widoku per repozytorium, a opinie nadal trafiają tam, gdzie wskazuje cel opinii Twojego zespołu. W Ustawieniach, Publiczny kanał jest selektor, który wpisze ten atrybut za Ciebie.

Renderuje trzy zakładki w tej kolejności: Updates, Roadmap i Feedback. Pierwsze dwie czytają powyższe feedy. Trzecia wysyła do poniższego endpointu i zapisuje identyfikator każdego zgłoszenia w localStorage, więc odwiedzający może wrócić i zobaczyć, co się stało z tym, co wysłał.

Skrypt jest serwowany z wersjonowaniem. /widget.js zawsze serwuje najnowszy build i jest cachowany przez godzinę, więc wydanie dociera do Twoich odwiedzających bez dotykania czegokolwiek. /widget-vN.js przypina jeden build: gdy numer wersji zostanie już zaserwowany, jego bajty nigdy się nie zmieniają, i jest cachowany przez rok. Przypnij, jeśli wolisz celowo przyjmować zmiany.

Wczytaj dokładnie jeden skrypt widgetu na stronę

Oba adresy URL to alternatywy, nie warstwy. Oba rejestrują tę samą nazwę custom elementu, a przeglądarka pozwala zarejestrować nazwę tylko raz na dokument: ten, który wykona się pierwszy, wygrywa na czas życia strony, a drugi staje się nieaktywny. Więc strona z /widget.js i /widget-v5.js renderuje ten, który przeglądarka wykonała pierwszy, co nie jest czymś, co kontrolujesz, a dodanie /widget-v5.js obok istniejącego /widget.js, by przypiąć wersję, nic nie robi.

Gdy tak się dzieje, widget zapisuje w konsoli ostrzeżenie z nazwami obu buildów, więc nie zostajesz z zgadywaniem. Nie może zrobić nic więcej niż ostrzec: gdy druga kopia się wykonuje, pierwsza już zarezerwowała nazwę. Poprawką jest zawsze zastąpienie tagu skryptu zamiast dodawania kolejnego, i to samo dotyczy sytuacji, gdy menedżer tagów lub fragment wstawi go za Ciebie. Aby przejść z buildu ciągłego na przypięty, zmień src.

Hostowana strona feedu

https://feed.changeloop.dev/feed/YOUR_PUBLIC_ID

Pod tym adresem hostujemy też zwykłą stronę: Twój changelog i tablicę roadmapy, renderowane z tych samych dwóch feedów powyżej. Nie wymaga logowania ani niczego skonfigurowanego po Twojej stronie. To także miejsce, do którego odsyłamy ludzi, gdy pętla się zamyka: komentarz Shipped, który zostawiamy na issue na GitHubie, prowadzi tutaj, podobnie jak shippedEntry.link z wyszukiwania zgłoszenia powyżej, oba lądują na dostarczonym wpisie z własną kotwicą #entry-ID, która nadal znajdzie wpis, nawet jeśli przeniósł się na późniejszą stronę.

Traktuj to jako rozwiązanie zapasowe, nie integrację. Feed changeloga i widget nadal są sposobem na umieszczenie tego na Twojej własnej stronie, by wyglądało jak Twój produkt, a nie nasz; ta strona jest dla sytuacji, gdy jeszcze tego nie zrobiłeś, i dla linków zamykających pętlę, które prowadzą tutaj niezależnie od tego, co jeszcze zbudowałeś.

Własna domena

Możesz serwować hostowaną stronę z własnego adresu, bez zmian w DNS ani certyfikatach. W Ustawieniach, sekcja Własna domena, wklej publiczny adres, który zobaczą czytelnicy (na przykład https://example.com/changelog), a następnie skieruj tę ścieżkę w swojej witrynie na pokazany tam cel proxy: jedna reguła obejmuje stronę, jej zasoby, dane i feedy. Sprawdź moją domenę pobiera Twój adres z naszej strony i mówi, czy proxy jest poprawne, a jeśli nie, co zmienić.

Serwer MCP

POSThttps://api.changeloop.dev/mcp

Jeśli pracujesz w Claude Code, ChatGPT lub innym agencie mówiącym Model Context Protocol, możesz połączyć go bezpośrednio z Twoim changelogiem. Agent może wtedy zobaczyć, co czeka na recenzję, edytować tekst i publikować, bez opuszczania edytora. To ta sama brama recenzji co w aplikacji webowej: nic nie staje się publiczne, dopóki coś tego nie zatwierdzi.

Łączenie Claude Code

Najpierw utwórz klucz API (Ustawienia, Klucze API), a potem dodaj serwer ze swoim kluczem w nagłówku:

claude mcp add --transport http changeloop \
  https://api.changeloop.dev/mcp \
  --header "Authorization: Bearer clapi_YOUR_KEY"

Dla klienta, który zamiast tego czyta konfigurację JSON, to samo wygląda tak:

{
  "mcpServers": {
    "changeloop": {
      "type": "http",
      "url": "https://api.changeloop.dev/mcp",
      "headers": { "Authorization": "Bearer clapi_YOUR_KEY" }
    }
  }
}

Nie ma jeszcze przepływu OAuth. Uwierzytelnianiem jest klucz API w nagłówku, co robią dwie powyższe komendy. Odwołanie tego klucza w Ustawieniach odłącza agenta przy jego następnym żądaniu.

Co może zrobić agent

Siedem narzędzi, i lista jest celowo krótka. Wszystko inne, co może zrobić ten produkt, jest osiągalne przez REST API tym samym kluczem; każde narzędzie ujawnione agentowi to jedna rzecz więcej, do której wywołania można go namówić.

  • list_pending_entries, list_published_entries, get_entry - czytają Twoje wpisy. Oczekujące nie są publiczne.
  • update_entry - zmienia tytuł lub treść markdown wpisu. HTML serwowany przez feed jest ponownie renderowany z Twojego markdownu przez nasz sanitizer; agent nie może dostarczyć HTML.
  • approve_entry - publikuje. To publiczne i natychmiastowe, i powiadamia wszelkie powiązane opinie na GitHubie. Tylko oczekujący wpis może zostać zatwierdzony.
  • discard_entry - trzyma wpis poza changelogiem. Odwracalne z aplikacji webowej.
  • get_changelog_info - identyfikator Twojego feedu i adresy, pod którymi serwowany jest Twój changelog.

Czego nie może zrobić

Każde narzędzie jest ograniczone do zespołu, do którego należy klucz, i żadne nie przyjmuje zespołu jako argumentu, więc nie ma niczego, co wskazywałoby na inny zespół, nawet gdyby coś próbowało. Serwer nie akceptuje sesji przeglądarki, tylko klucz: żądanie musi celowo dołączyć poświadczenie. A klucz nie może zarządzać kluczami ani pobrać eksportu Twoich danych, więc agent połączony w ten sposób nie może wybić sobie drugiego poświadczenia ani wyciągnąć Twoich danych w jednym wywołaniu.

Klucze API

Wszystko powyżej jest anonimowe i nie wymaga poświadczenia. Uwierzytelnione API - Twoje ustawienia, Twoja skrzynka recenzji - to inna powierzchnia i akceptuje albo zalogowaną sesję przeglądarki, albo klucz API. Klucze są dla skryptów i agentów: wszystkiego, co musi dotrzeć do Twojego changeloga bez osoby przy klawiaturze.

Authorization: Bearer clapi_YOUR_KEY

Utwórz jeden w aplikacji w Ustawieniach, na zakładce Klucze API. Klucz jest pokazywany raz, w momencie utworzenia, i nigdy więcej: przechowujemy tylko jego hash, więc nie ma żadnego ekranu, który mógłby go pokazać po raz drugi. Jeśli go zgubisz, odwołaj go i utwórz kolejny.

Co klucz może i czego nie może zrobić

Klucz niesie ten sam dostęp co zalogowanie, ograniczony do jednego zespołu, w którym został utworzony, z dwoma celowymi wyjątkami. Nie może zarządzać kluczami API i nie może pobrać eksportu Twoich danych. Oba wymagają prawdziwego logowania, tak by wyciekły klucz nie mógł wybić sobie zastępników, nie mógł odwołać kluczy, których użyłbyś, by go zablokować, i nie mógł wyciągnąć danych Twojego zespołu w jednym żądaniu.

Odwoływanie

Odwołanie wchodzi w życie przy następnym żądaniu. Odwołany klucz odpowiada 401 dokładnie tak jak nieznany, i wciąż odpowiada 401 nawet z przeglądarki, która wciąż ma ważną sesję, ponieważ żądanie niosące nagłówek Authorization nigdy nie jest cicho ponawiane jako żądanie z ciasteczkiem. Odwołany klucz pozostaje na liście z datą odwołania i datą ostatniego użycia, co jest tym, czego chcesz, gdy ustalasz, dokąd dotarł wyciekły klucz.

Plany i limity

Darmowy plan opisuje 20 scalonych zmian miesięcznie i ogranicza dzienną liczbę przejrzanych scaleń, przetworzonych zgłoszeń opinii, przygotowanych kart roadmapy i wersji alternatywnych do 50 każda; plan zespołowy nie ma sztywnych limitów. Ustawienia, sekcja Plan i użycie, pokazuje każdy budżet tak, jak liczy go sam produkt, wraz z momentem resetu, zanim cokolwiek zostanie odrzucone. Praca, która nadejdzie ponad limit, jest wstrzymywana, nie tracona: wpis ponad limit czeka w skrzynce, a odrzucony szkic roadmapy można ponowić, gdy okno się odnowi.

GitLab i Bitbucket

Projekt GitLab lub repozytorium Bitbucket może karmić Twój changelog tak samo jak repozytorium GitHub: połącz w Ustawieniach, potem GitLab, albo w Ustawieniach, potem Bitbucket, dodaj podany przez nas webhook (albo na bitbucket.org pozwól, by dodał go Connect with Bitbucket, jeśli strona Bitbucket oferuje ten przycisk), a każda zmiana połączona z podaną przez Ciebie gałęzią staje się szkicem wpisu w Twojej skrzynce recenzji, napisanym w ten sam sposób i podlegającym tej samej ludzkiej recenzji. Wpisy powstają z połączonych pull requestów lub merge requestów albo, na GitHubie i Bitbuckecie, z pushy, jeśli wybierzesz tryb push w Ustawieniach, potem What creates drafts. Projekty GitLab tworzą szkice wyłącznie z merge requestów.

Łączenie projektu

Projekty GitLab łączysz w Ustawieniach, potem GitLab, a repozytoria Bitbucket w Ustawieniach, potem Bitbucket. Wpisz ścieżkę (w GitLabie grupę i projekt, np. acme/web, w Bitbuckecie workspace i repozytorium, np. acme/app), a my zwrócimy adres webhooka i sekret. Wklej oba do ustawień webhooka po ich stronie: w GitLabie zaznacz Merge request events, w Bitbuckecie zaznacz wyzwalacze Merged pull request i Push repository. Instancje samodzielnie zarządzane działają, przez https. Sekret jest pokazywany raz, w tym momencie. Jeśli go zgubisz, usuń projekt i połącz ponownie. Na bitbucket.org, jeśli strona Bitbucket pokazuje przycisk Connect with Bitbucket, możesz pominąć wklejanie: kliknij go, raz zezwól na dostęp, a my odczytamy główną gałąź repozytorium i dodamy webhook za Ciebie. Potrzebujesz uprawnień administratora repozytorium. Przy samodzielnie zarządzanym Bitbuckecie, albo jeśli wolisz wkleić, wybierz Set it up by hand, a dostaniesz adres i sekret jak wyżej. Jeśli usuniesz repozytorium z Bitbucketa i połączysz je ponownie, usuń też jego stary webhook na Bitbuckecie, w Repository settings, potem Webhooks. Po połączeniu projektu możesz w jego wierszu zmienić gałąź i włączyć automatyczną publikację, a jeśli jakaś dostawa została zignorowana, wiersz podaje powód.

Dlaczego Bitbucket pyta o gałąź, a GitLab nie

GitLab mówi nam, którą gałąź Twój projekt traktuje jako domyślną, więc możesz zostawić pole puste i to znaczyć. Bitbucket w ogóle nie wysyła domyślnej gałęzi, więc gdybyśmy pozwolili Ci zostawić puste, nie mielibyśmy z czym porównać, a Twój webhook wyglądałby na doskonale zainstalowany, nigdy nie produkując ani jednego wpisu. Wolimy zadać jedno pytanie niż na to pozwolić. Z Connect with Bitbucket pytamy Bitbucket o główną gałąź, gdy zezwolisz na dostęp, więc nie musisz jej wpisywać.

Czego jeszcze nie obejmują

Wpisów changeloga i nic więcej. Widget opinii otwierający issue za Ciebie, odpowiedź publikowana na tym issue, gdy poprawka jest dostarczona, publiczna roadmapa napędzana etykietami issue i podgląd źródła w skrzynce recenzji są dziś dostępne tylko dla GitHuba.

Powód jest taki, który wolimy powiedzieć wprost niż ukrywać. Każde z tych rozwiązań potrzebuje tokenu dostępu z uprawnieniami zapisu do Twojego projektu, przechowywanego przez nas. Wpisy changeloga nie potrzebują żadnego, ponieważ wszystko, z czego są pisane, przychodzi w samym webhooku, więc połączenie GitLaba lub Bitbucketa przez webhook nie daje nam żadnego poświadczenia ani odczytu Twojego kodu. Connect with Bitbucket to jedyny wyjątek. Bitbucket użycza nam na jedno żądanie tokenu, który może odczytywać repozytorium i jego pull requesty oraz zarządzać jego webhookami. Używamy go tylko do odczytania głównej gałęzi i dodania webhooka, a potem go usuwamy. Nic nie jest przechowywane. Wolimy dostarczyć część, która nic Cię nie kosztuje, niż prosić o token, by uzupełnić listę funkcji.

Inne wersje wpisu

Jedna zmiana zwykle musi być wyjaśniona więcej niż raz: klientom w changelogu, komukolwiek, kto odpowiada na pytania o nią, i na kanale, gdzie nikt nie czyta czterech akapitów. Ze skrzynki recenzji możesz zredagować jedną z dwóch dodatkowych wersji wpisu, zanim go zatwierdzisz.

Wersja ogłoszenia to jedna lub dwie linie, i to jest publikowane na Slacku, gdy zatwierdzasz wpis, zamiast pełnego tekstu. Notatka wsparcia to wewnętrzny briefing: co się zmieniło, co zauważą klienci, i zdanie, które agent mógłby powiedzieć niemal dosłownie. Obie to szkice, które możesz przepisać przed użyciem, i obie można usunąć.

Żadna z nich nie jest publikowana

Te wersje nigdy nie pojawiają się na Twojej stronie changeloga, w żadnym feedzie, w widgecie ani w API, które je serwuje. Notatka wsparcia w szczególności jest pisana dla osób w Twojej firmie i może być bardziej bezpośrednia niż sam wpis. Jedyne miejsca, w których istnieje, to Twoja skrzynka recenzji i, jeśli ich używasz, Twoja własna kopia.

Z czego są pisane

Zawsze z wpisu, nigdy z pull requesta. To celowe: wpis przeszedł już przez regułę, która utrzymuje poprawki bezpieczeństwa w niejasności, i przez Twoją własną recenzję. Wersja przepisana z niego nie może ponownie wprowadzić szczegółu, który usunąłeś, ponieważ tego szczegółu nie ma w tym, co dostał model.

Ogłaszanie na Slacku

Zatwierdź wpis, a może zostać opublikowany na kanale Slacka w tym samym momencie, w którym stanie się publiczny. Połącz w Ustawieniach, na zakładce Slack: utwórz przychodzący webhook w swoim własnym workspace, wybierz kanał i wklej URL. Nic nie jest instalowane po Twojej stronie poza tym webhookiem, i nie prosimy o żaden dostęp do Twojego workspace.

Wiadomość niesie tytuł wpisu, tekst taki, jaki zatwierdziłeś, jego kategorię i tagi, oraz link z powrotem do wpisu w Twoim changelogu. Markdown jest tłumaczony na to, co Slack faktycznie renderuje, więc wpis nie przychodzi pokazując własne gwiazdki.

URL webhooka to poświadczenie

Każdy, kto ma ten URL, może publikować na kanale, więc traktujemy go jak hasło: jest przechowywany, a potem żaden ekran ani żadna odpowiedź API go już nie pokazuje, w tym Twój własny eksport danych. To, co widzisz potem, to maska, wystarczająca, by odróżnić dwa webhooki, i bezużyteczna dla kogokolwiek innego. Akceptujemy tylko adres hooks.slack.com, więc źle wpisany lub podstawiony URL jest odrzucany, a nie pobierany.

Gdy przestaje działać

Jeśli usuniesz aplikację na Slacku lub zarchiwizujesz kanał, webhook trwale przestaje działać. Zauważamy to przy pierwszej odrzuconej wiadomości, wyłączamy ogłoszenia i podajemy to na zakładce Slack z powodem i datą. Celowo nie próbujemy dalej po cichu: changelog, którego nikt nie ogłosił, wygląda dokładnie jak taki, którego nikt nie przeczytał, a to różnica warta poinformowania.

Wstrzymywanie

Wstrzymaj zatrzymuje ogłoszenia i zachowuje webhook, więc wznowienie to jedno kliknięcie zamiast kolejnej rundy przez Slacka. Rozłącz usuwa URL całkowicie. Tak czy inaczej, samo publikowanie nie jest dotknięte: Slack to kanał, na który publikuje Twój changelog, nigdy brama, na którą czeka. Jeśli Slack jest niedostępny, gdy coś zatwierdzasz, wpis nadal się publikuje, a ogłoszenie jest ponawiane samodzielnie.

RSS i JSON Feed

GEThttps://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/rss.xmlGEThttps://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/feed.json

Te same opublikowane wpisy jako feed do subskrypcji, w dwóch formatach zrozumiałych dla czytników: RSS 2.0 i JSON Feed 1.1. Oba przyjmują te same filtry repos, category i tag co feed changeloga i niosą ten sam Cache-Control i ETag. Żaden nie paginuje: czytnik odpytuje początek feedu, więc te zwracają tylko najnowsze wpisy, bez kursora.

Tekst wpisu to zsanityzowany HTML, opakowany w CDATA dla RSS i jako content_html dla JSON Feed. JSON Feed dodatkowo niesie kolory Twoich tagów pod rozszerzeniem z przestrzenią nazw _changelogapp; RSS nie, ponieważ żaden czytnik by ich nie pomalował.

Hostowana strona reklamuje oba jako linki rel="alternate", więc przeglądarka lub czytnik, który tam trafi, może zasubskrybować bez podawania ścieżek.

Jeden wpis osobno

GEThttps://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/entries/ENTRY_ID

Zwraca pojedynczy opublikowany wpis, ten sam obiekt, który feed changeloga niesie w swojej tablicy data. To tam prowadzą stałe linki w feedach, i jest przydatne, gdy masz identyfikator i nie chcesz paginować feedu, by go znaleźć. Nieznany identyfikator lub taki, który należy do niepublikowanego wpisu, zwraca 404 z tą samą treścią co każdy inny nieznany identyfikator.

Feed markdown

GEThttps://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/changelog.md

Te same opublikowane wpisy jako zwykły markdown, serwowany jako text/markdown. Istnieje dla czytników, które nie są przeglądarkami: LLM lub agent odpowiadający na "co ostatnio zmieniło się w tym produkcie" dostaje tekst bez parsowania RSS lub przechodzenia po JSON. Przyjmuje te same filtry repos, category i tag co feed changeloga, niesie ten sam Cache-Control i ETag, i odpowiada 304 na żądanie warunkowe dokładnie jak dwa pozostałe.

Każdy wpis to sekcja: tytuł jako nagłówek, potem jedna linia z datą, kategorią i wszelkimi tagami, potem tekst wpisu tak, jak został napisany, potem link Learn more, jeśli wpis go ma, potem jego stały link. Dokument zaczyna się tytułem i opisem Twojego feedu i prowadzi z powrotem do hostowanej strony. Gdy nic jeszcze nie zostało opublikowane, mówi to w jednym zdaniu zamiast zwracać pustą treść, więc czytnik może odróżnić to od nieudanego pobrania.

Hostowana strona reklamuje go jako link rel="alternate" z type text/markdown, obok linków RSS i JSON Feed, więc agent, który pobrał HTML, może go znaleźć bez podawania ścieżki.

To, co serwuje, to markdown, który przygotowaliśmy, a Ty zatwierdziłeś, nie zsanityzowany HTML. Jest to bezpieczne jako markdown, który jest obojętny, i dlatego ta odpowiedź nigdy nie jest text/html. Jeśli renderujesz go sam, escapuj go tak, jak escapowałbyś każdy inny niezaufany markdown: wpisy tworzone z publicznego repozytorium mogą być pod wpływem każdego, kto może tam otworzyć pull request.

Zbieranie opinii z Twojej własnej strony

Dodaj swoje pochodzenia przed testowaniem tego

To jedyny endpoint w produkcie, który zapisuje, więc nie akceptuje żądań skądkolwiek. Dopasowuje nagłówek Origin przeglądarki do listy dozwolonych per zespół, a ta lista zaczyna się pusta. Pusta oznacza odrzuć wszystko, nie zezwól na wszystko. Dopóki nie dodasz pochodzenia, w którym osadzasz, każde zgłoszenie wraca z 403 i {"error":"origin_not_allowed"}, i nic nie dociera do Twojej skrzynki. Jeśli Twój formularz wygląda poprawnie i nadal zawodzi, prawie zawsze to jest powód. Ustaw listę uwierzytelnionym PATCH do /v1/settings/feed niosącym {"allowedOrigins": ["https://your-site.example"]}, i odczytaj z powrotem GET-em na tę samą ścieżkę, który odpowiada Twoim publicId, allowedOrigins oraz feedTitle i feedDescription, które widzą Twoi subskrybenci w czytniku feedów. Przechowujemy każde pochodzenie dokładnie w formie, w jakiej wysyła je przeglądarka, więc końcowy ukośnik lub jawny domyślny port w tym, co wysyłasz, nie stanowi problemu.

POST/v1/public/YOUR_PUBLIC_ID/feedback
POST https://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/feedback
Content-Type: application/json
Origin: https://your-site.example

{ "email": "someone@example.com", "message": "Dark mode, please." }

202 Accepted
{ "publicSubmissionId": "0ZbQ8yqk3n7T1sVJ4mWpLd2rXfEuGh6A" }

email musi wyglądać jak adres e-mail i mieć 254 znaki lub mniej. message musi być niepuste i mieć 2KB lub mniej, mierzone w bajtach UTF-8, nie znakach. Cała treść JSON jest ograniczona do 8KB. Jest jeszcze jedno pole, website: to honeypot, więc pomiń je lub wyślij puste, jeśli renderujesz je jako ukryte pole tak jak robi to nasz widget.

Warto zrozumieć honeypot, zanim zaczniesz cokolwiek nim debugować. Jeśli website przybędzie z czymś wpisanym, odpowiadamy 202 z zupełnie normalnie wyglądającym identyfikatorem zgłoszenia, a potem nic nie robimy, ponieważ bot, który dowiaduje się, że został złapany, po prostu spróbuje ponownie inaczej. To właściwa odpowiedź dla bota i myląca dla Ciebie, więc jeśli Twój własny formularz ma pole o nazwie website, które przeglądarka mogłaby autouzupełnić, zmień jego nazwę lub usuń je. Zgłoszenie, które wygląda na przyjęte i nigdy się nie pojawia, prawie zawsze jest tym.

Przyjęte przez nas zgłoszenie zwraca 202 z publicSubmissionId. Oddaj to osobie, która je wysłała, i zachowaj, jeśli możesz: to jedyny sposób, w jaki może sprawdzić, co się dalej stało.

Tryby błędów to 400 z invalid_email lub invalid_message dla złego kształtu, 413 z email_too_large lub message_too_large dla dobrego kształtu, ale zbyt dużego, 429 z rate_limited powyżej 5 zgłoszeń na minutę lub 30 na godzinę z jednego adresu do jednego feedu, 403 z origin_not_allowed i 404 z not_found dla nierozpoznanego identyfikatora feedu.

Jest też dzienny limit per zespół na to, ile pracy poniżej mogą wywołać zgłoszenia. Po jego przekroczeniu nadal przyjmujemy i przechowujemy wszystko, co przychodzi, po prostu czeka, aż ktoś z Twojego zespołu na to spojrzy, zamiast otwierać cokolwiek samodzielnie.

Sprawdzanie jednego zgłoszenia

GET/v1/public/YOUR_PUBLIC_ID/feedback/PUBLIC_SUBMISSION_ID

Odpowiada statusem, plus githubIssueUrl, gdy tylko dla tego zgłoszenia istnieje issue, plus shippedEntry niosącym tytuł i link, gdy tylko praca jest gotowa. Adres e-mail zgłaszającego nigdy nie jest odczytywany z naszej bazy danych dla tej trasy, a tym bardziej zwracany, co czyni odpowiedź bezpieczną do renderowania na stronie widocznej dla każdego. Identyfikator jest całym poświadczeniem, traktuj go jako takie. Jest ograniczony do 20 żądań na minutę i 200 na godzinę na adres i feed.

Cache, CORS i żądania warunkowe

Oba feedy wysyłają Cache-Control: public, max-age=60, stale-while-revalidate=300 wraz z silnym ETagiem. Odeślij ten ETag jako If-None-Match, a niezmieniony feed odpowie 304 bez treści. Żadne pole odpowiedzi nie niesie wartości zegara ściennego, więc ETag pozostaje stabilny, gdy ponownie renderujemy dane, które się nie zmieniły, co czyni te 304 wartymi zaufania.

Oba feedy i wyszukiwanie zgłoszenia to anonimowe odczyty i odpowiadają z Access-Control-Allow-Origin: *, więc możesz je wywoływać z dowolnego pochodzenia, z curla lub z kroku builda. POST opinii jest wyjątkiem: odpowiada Twoim własnym dozwolonym pochodzeniem i Vary: Origin, nigdy z symbolem wieloznacznym. Przeglądarki robią na nim preflight, a preflight zawsze odpowiada 204 niezależnie od tego, czy pochodzenie jest dozwolone, więc nie może być użyty do sondowania Twoich ustawień.