10 хв читанняІнтеграції

Скорочувач URL для GitHub Actions: короткі посилання з CI

Створюйте й оновлюйте короткі посилання з GitHub Actions: API-ключ як зашифрований секрет, робочий workflow, ідемпотентні upsert-операції та ключі з мінімальними привілеями.

Marius Voß
DevRel · edge infra
Крок скорочення URL у GitHub Actions, зображений як конвеєр: запуск workflow читає зашифрований секрет, знаходить слаг, а потім оновлює наявне коротке посилання або створює нове

Крок скорочення URL у GitHub Actions - це кілька рядків shell: прочитати API-ключ із зашифрованого секрету, перевірити, чи вже існує слаг, а потім або оновити його призначення, або створити посилання. Запускайте це під час кожного push - і те саме коротке посилання завжди вказуватиме на найновіший preview, збірку документації або артефакт. Marketplace action не потрібен. curl і jq є на кожному Ubuntu-раннері GitHub.

Ось і вся відповідь, а решта цієї статті пояснює, як зробити так, щоб вона витримувала кількасот запусків workflow. Люди, які шукають, як створити коротке посилання в GitHub Actions, зазвичай доходять до одного POST-запиту, і він працює рівно до другого push у той самий pull request, коли запит створення повертає конфлікт, а завдання стає червоним. Виправлення - трактувати цей крок як upsert, а не як створення. Інша проблема - сам ключ: часто це персональний токен із набагато ширшими повноваженнями, ніж потрібно CI-завданню.

Якщо ви вже керуєте посиланнями як кодом, короткі посилання через Terraform - декларативна версія тієї самої ідеї, яка краще підходить для посилань, що змінюються за розкладом людини. Крок workflow кращий, коли призначення з'являється лише після завершення збірки.

Як працює крок скорочувача URL у GitHub Actions

Кожен запуск виконує ті самі три дії щодо REST API за адресою https://api.elido.app/v1. Він отримує посилання робочого простору, відфільтровані за слагом. Надсилає PATCH для знайденого посилання або POST, якщо нічого не знайдено. Записує короткий URL у $GITHUB_OUTPUT, щоб його міг використати наступний крок.

Чому б не дозволити скорочувачу генерувати випадковий слаг? Тоді ви не зможете знову знайти це посилання. Слаг має походити з того, що workflow знає під час кожного запуску: номера pull request, назви гілки, фіксованого слова на кшталт latest. Стабільний слаг означає стабільний короткий URL, і саме це цінують рецензенти, які додають його в закладки, або продакт-менеджери, які вставляють його в задачу.

Як працює крок скорочення URL у GitHub Actions: workflow читає API-ключ із зашифрованого секрету, отримує посилання за слагом, надсилає PATCH, коли слаг існує, або POST, коли його немає, а потім записує короткий URL у вихідне значення кроку

Зберігання API-ключа як зашифрованого секрету

Створіть ключ у dashboard, скопіюйте його один раз (його показують рівно один раз, і він починається з elido_) та збережіть у Settings, потім Secrets and variables, потім Actions, під назвою ELIDO_API_KEY. Посібник GitHub про використання секретів у GitHub Actions охоплює рівні репозиторію, середовища й організації. Для всього, що розгортається, я б розмістив його в середовищі з обов'язковими рецензентами, щоб випадкова гілка не могла ним скористатися.

Три значення не є секретами й мають зберігатися у змінних конфігурації, де їх можна прочитати: ELIDO_WORKSPACE_ID, ELIDO_DOMAIN_ID і ELIDO_HOST. ID домену важливий, бо він потрібен у запиті створення. Його можна один раз отримати через GET /v1/workspaces/{workspace_id}/domains, який повертає id і hostname кожного домену.

Передайте секрет в один крок, який викликає API, а не в усе завдання. env на рівні кроку не дає йому потрапити в інші процеси, запущені завданням, зокрема в сторонні actions, яких ви не писали.

Робочий workflow для скорочення URL у кожному pull request

Це повний файл для найпоширенішої причини скорочувати URL у GitHub workflow: окреме preview-посилання для кожного pull request. Додайте його до .github/workflows/preview-link.yml і змініть рядок DEST на адресу, за якою розгортається ваш preview.

name: Preview short link

on:
  pull_request:
    types: [opened, reopened, synchronize]

permissions:
  contents: read
  pull-requests: write

concurrency:
  group: preview-link-${{ github.event.pull_request.number }}
  cancel-in-progress: true

jobs:
  short-link:
    # Forks get no secrets; skip them instead of failing.
    if: github.event.pull_request.head.repo.full_name == github.repository
    runs-on: ubuntu-latest
    env:
      API: https://api.elido.app/v1/workspaces/${{ vars.ELIDO_WORKSPACE_ID }}
      DOMAIN_ID: ${{ vars.ELIDO_DOMAIN_ID }}
      HOST: ${{ vars.ELIDO_HOST }}
      SLUG: pr-${{ github.event.pull_request.number }}-myapp
      DEST: https://pr-${{ github.event.pull_request.number }}.preview.example.com
    steps:
      - name: Create or update the short link
        id: link
        env:
          ELIDO_API_KEY: ${{ secrets.ELIDO_API_KEY }}
        run: |
          set -euo pipefail
          auth=(-H "Authorization: Bearer $ELIDO_API_KEY" -H "Content-Type: application/json")

          # 1. Find an existing link with exactly this slug on this domain.
          link_id=$(curl -sS --fail-with-body "${auth[@]}" "$API/links?q=$SLUG&limit=100" \
            | jq -r --arg s "$SLUG" --argjson d "$DOMAIN_ID" \
                '.items[] | select(.slug == $s and .domain_id == $d) | .id' | head -n1)

          if [ -n "$link_id" ]; then
            # 2a. Found: point it at the new destination.
            curl -sS --fail-with-body -X PATCH "${auth[@]}" "$API/links/$link_id" \
              -d "$(jq -n --arg u "$DEST" '{destination_url: $u, status: "active"}')" > /dev/null
          else
            # 2b. Not found: create it. The key makes curl's retries safe.
            curl -sS --fail-with-body --retry 3 -X POST "${auth[@]}" "$API/links" \
              -H "Idempotency-Key: $GITHUB_REPOSITORY-$SLUG-$GITHUB_RUN_ID" \
              -d "$(jq -n --arg u "$DEST" --arg s "$SLUG" --argjson d "$DOMAIN_ID" \
                  '{domain_id: $d, slug: $s, destination_url: $u, tags: ["ci", "preview"]}')" > /dev/null
          fi

          echo "url=https://$HOST/$SLUG" >> "$GITHUB_OUTPUT"

      - name: Comment once, when the pull request opens
        if: github.event.action == 'opened'
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          gh pr comment "${{ github.event.pull_request.number }}" \
            --repo "${{ github.repository }}" \
            --body "Preview: ${{ steps.link.outputs.url }}"

Кілька рядків потребують пояснення. --fail-with-body перетворює 4xx або 5xx на невдалий крок і водночас виводить тіло помилки, чого не робить звичайний curl -s; він завершується з кодом 0 для 401, і завдання спокійно продовжується. Тіла запитів будуються через jq -n, а не інтерполяцію рядків, тому призначення з лапками або амперсандом не зламає JSON. А крок коментаря запускається лише для opened. Оскільки короткий URL не змінюється, одного коментаря достатньо на весь час існування PR, і ніхто не отримуватиме сповіщення під час кожного push.

Блок concurrency - не декорація. Інакше два швидкі push запустили б два процеси, які обидва побачили б "посилання ще немає" і обидва спробували б створити його. Документація GitHub про керування одночасністю workflow пояснює групування; тут старий запуск скасовується, і змагання не відбувається.

Ідемпотентність: оновлюйте коротке посилання, а не дублюйте його

Під словом idempotent ховаються дві різні помилки, і workflow обробляє їх окремо. Перша - повторний запуск: другий push, ручне "Re-run jobs", повторно відкритий PR. Саме для цього існує гілка пошуку з подальшим PATCH. Друга - повторно надісланий запит: curl надсилає POST, мережа обривається до отримання відповіді, і curl надсилає його знову. Заголовок Idempotency-Key покриває цей випадок. Elido зберігає першу успішну відповідь для ключа протягом 24 годин і відтворює її для відповідного повторного запиту, тому створення виконується один раз; повний механізм описано в матеріалі про ліміти швидкості, повторні спроби та ідемпотентність.

Потік прийняття рішення для створення короткого посилання в GitHub Actions без дублікатів: точний збіг слага у вашому робочому просторі веде до PATCH, відсутність збігу - до POST, а 409 означає, що інший робочий простір на спільному домені вже володіє слагом

Зіткнення слагів - це те, що люди часто пропускають. Слаги унікальні для домену перенаправлення, а не для робочого простору. На спільному домені всі інші клієнти Elido перебувають у тому самому просторі імен, і такий простий слаг, як pr-12, імовірно, уже кимось зайнятий. Пошук не побачить їхнього посилання (він показує лише ваш робочий простір), тому POST буде надіслано і повернеться 409 slug already exists for this domain. Є два виправлення: додайте до слага слово проєкту або розмістіть CI-посилання на власному кастомному домені, де простір імен належить лише вам. Я б зробив і те, і те.

Є ще одна, підступніша причина, чому назва репозиторію стоїть у кінці слага, а не на початку. Параметр q шукає частковий збіг у слазі, призначенні й заголовку. Для myapp-pr-1 пошук також повертає myapp-pr-10 до myapp-pr-199, що перевищує 100 результатів на одній сторінці, і потрібне вам посилання, як найстаріше, випадає з кінця. pr-1-myapp збігається лише саме з собою. Дрібниця, але щоб її помітити, мені знадобився ганебно довгий день роздумів: "чому PR #1 постійно отримує 409".

Три CI-завдання для коротких посилань, які варто автоматизувати

Workflow для preview - лише один шаблон. Змініть тригер, слаг і призначення, і той самий крок покриє більшість того, що команди справді автоматизують. (Нотатки до релізу - окрема тема, описана в матеріалі про скорочення посилань у нотатках до релізу.)

Варіант використанняТригерСлагЩо робить крок
Preview-розгортання для кожного PRpull_requestpr-42-myappUpsert під час кожного push, видалення після закриття
Розгортання документаціїpush до maindocs-myappPATCH до щойно розгорнутого URL документації
Найновіша збіркаpush до main або тегуlatest-myappPATCH за збереженим ID посилання, без пошуку
Нічний артефактschedulenightly-myappPATCH до URL найновішого артефакту

Випадок із найновішою збіркою - найпростіший із усіх. Один раз створіть посилання вручну, збережіть його числовий ID як змінну, і завдання скоротиться до одного виклику:

- name: Point the latest link at this build
  env:
    ELIDO_API_KEY: ${{ secrets.ELIDO_API_KEY }}
    API: https://api.elido.app/v1/workspaces/${{ vars.ELIDO_WORKSPACE_ID }}
    DEST: https://builds.example.com/${{ github.sha }}/
  run: |
    curl -sS --fail-with-body -X PATCH \
      -H "Authorization: Bearer $ELIDO_API_KEY" -H "Content-Type: application/json" \
      "$API/links/${{ vars.ELIDO_LATEST_LINK_ID }}" \
      -d "$(jq -n --arg u "$DEST" '{destination_url: $u}')"

Для таких рухомих посилань залишайте 302, який використовується за замовчуванням, якщо не задати redirect_status. 301 повідомляє браузерам, що відповідь можна кешувати, і люди, які клікнули вчора, і далі потраплятимуть на вчорашню збірку; розгорнутий матеріал про перенаправлення 301 і 302 містить деталі.

Для preview-посилань приберіть їх після закриття PR. Додайте closed до типів тригера, повторно використайте пошук і надішліть DELETE /v1/workspaces/{workspace_id}/links/{link_id}. Видалений слаг можна використовувати знову. Якщо ви хочете зберегти історію кліків, натомість виконайте PATCH із {"status": "disabled"}; наведений вище upsert під час кожного запуску встановлює status: "active", тому повторно відкритий PR повертає своє посилання.

Готові спробувати це в одному репозиторії? Створіть безкоштовний робочий простір, створіть ключ - і наведений workflow працюватиме як є, щойно буде задано три змінні.

API-ключі для CI з мінімальними привілеями

Ключ у секреті CI має вміти рівно те, що робить workflow, і нічого більше. Це складніше, ніж здається, через особливості роботи персональних ключів.

Персональний API-ключ автентифікує користувача, який його створив. Усе, що може робити ця людина, може робити й ключ, а коли вона залишає компанію, ключ залишається з її обліковим записом. Для CI я б натомість використав машинного користувача: сервісний обліковий запис, який належить одному робочому простору, має власну роль і токени, які може створювати або відкликати лише автентифікований адміністратор-людина. Створіть його в розділі Machine users у dashboard із роллю editor - це найнижча вбудована роль, яка може створювати, редагувати й видаляти посилання, - а потім випустіть для нього токен із датою завершення дії. Вимкнення машинного користувача одразу знищує всі його токени, і саме така кнопка потрібна в день витоку секрету.

Ще чотири звички нічого не коштують:

  • Один токен на репозиторій, названий на його честь, щоб в аудиторському журналі було видно, який репозиторій створив яке посилання.
  • Секрети середовища з обов'язковими рецензентами для будь-якого workflow, який змінює посилання, на які покладаються люди.
  • Явно задавайте permissions: на початку workflow, як у прикладі, щоб GITHUB_TOKEN отримував лише потрібні завданню дозволи.
  • Не використовуйте pull_request_target, щоб дістатися секрету з PR форків. Матеріал GitHub Security Lab про запобігання pwn-запитам показує, чому запуск неперевіреного коду поруч із токеном на запис закінчується погано.

Робочі простори також можуть обмежувати доступ до API списком дозволених IP-адрес. Це надійний контроль для self-hosted раннерів із фіксованим вихідним трафіком і майже марний для раннерів GitHub, чиї адреси походять із дуже великого пулу, що постійно змінюється. Власний довідник GitHub із безпечного використання вартий години, якщо ваші workflow торкаються production.

Що ламається на практиці

Більшість помилок походить із чотирьох місць, і кожна стає зрозумілою помилкою, якщо ввімкнено --fail-with-body. 404 під час кожного виклику зазвичай означає, що змінна з ID робочого простору неправильна або ключ належить іншому робочому простору. 400 із повідомленням domain_id is required означає, що змінна порожня, зазвичай через те, що її задано в іншому середовищі, ніж те, яке використовує завдання. 409 - це зіткнення спільного простору імен із розділу про ідемпотентність. А 429 означає, що ви перевищили ліміт швидкості для ключа; один upsert за запуск його не досягне, але матриця з п'ятдесяти завдань може.

Одна річ узагалі не є помилкою. Після PATCH відвідувач ще деякий час може потрапляти на старе призначення, бо перенаправлення кешуються поблизу відвідувача, щоб працювати швидше. Перевірка, яка одразу після оновлення очікує нове призначення, буде нестабільною. Зробіть опитування з коротким backoff або перевіряйте відповідь API.

Якщо ви хочете, щоб крок повідомляв про зміни назовні, поєднайте його з вебхуками для подій посилань, які спрацьовують, коли посилання змінюється, або з шаблонами curl і jq з посібника CLI для локального тестування до фіксації workflow. У довіднику API та SDK перелічено всі поля, які приймають кінцеві точки посилань.

Читайте опорний матеріал → Керуйте короткими посиланнями як Terraform

Схожі матеріали в блозі

Поширені запитання

Чи може GitHub Actions створювати короткі посилання?

Так. Крок workflow може викликати REST API будь-якого скорочувача за допомогою curl, який разом із jq попередньо встановлений на раннерах GitHub. Крок читає API-ключ із зашифрованого секрету, надсилає URL призначення і записує отриманий короткий URL у вихідне значення кроку, щоб наступні кроки могли опублікувати його в коментарі до pull request або зведенні завдання.

Як зберігати API-ключ скорочувача URL у GitHub Actions?

Збережіть його як зашифрований секрет репозиторію або середовища, а потім передайте в єдиний крок, якому він потрібен, через запис env на кшталт ELIDO_API_KEY: secrets.ELIDO_API_KEY у синтаксисі виразів. GitHub маскує це значення в журналах. Незакриті значення, як-от ID робочого простору та ID домену, зберігайте натомість у змінних конфігурації, щоб їх можна було читати.

Як не створювати дублікати коротких посилань під час кожного запуску workflow?

Зробіть цей крок upsert-операцією. Виведіть слаг зі стабільного значення, наприклад номера pull request, спочатку знайдіть його, а якщо посилання вже існує, надішліть PATCH, щоб змінити призначення. Створюйте посилання лише коли пошук нічого не повернув. Заголовок Idempotency-Key у запиті створення покриває окремий випадок повторно надісланого запиту після мережевого тайм-ауту.

Чому під час створення короткого посилання мій workflow отримує 409?

Цей слаг уже зайнятий у цьому домені. В Elido слаги унікальні для кожного домену перенаправлення, а спільний домен спільний для всіх робочих просторів, тож загальний слаг на кшталт pr-12, імовірно, уже існує. Додайте до слага префікс або суфікс проєкту або використайте власний кастомний домен, де весь простір імен належить вам.

Чи працюють кроки коротких посилань із pull request із форків?

Не зі звичайним тригером pull_request, оскільки GitHub не передає секрети репозиторію workflow, запущеним форком. Пропускайте завдання для форків за допомогою умови if для репозиторію head. Перехід на pull_request_target заради доступу до секрету небезпечний, оскільки workflow запускається з доступом на запис поруч із кодом, який ви не перевіряли.

Для посилання на найновішу збірку використовувати перенаправлення 301 чи 302?

Використовуйте 302 або 307. Браузери можуть кешувати 301 безстроково, тож відвідувачі, які повернуться, і далі потраплятимуть на стару збірку після того, як workflow перемістить посилання. Посилання Elido за замовчуванням використовують 302, якщо не задати redirect_status, і це правильний вибір для будь-якого посилання, призначення якого змінює конвеєр.

Спробуйте Elido

Вставте URL - отримайте коротке посилання

Без реєстрації. Посилання живе 30 днів. Зареєструйтесь, щоб зберегти назавжди.

Безкоштовно, без реєстрації · 2 на день

Спробуйте Elido

URL-скорочувач із хостингом у ЄС: власні домени, глибока аналітика, відкритий API. Безкоштовний тариф - без кредитної картки.

Теги
github actions url shortener
create short link in github actions
shorten url github workflow
preview deployments
ci/cd
api keys

Читати далі