2分で読了連携

GitHub ActionsのURL短縮: CIから短縮リンクを作成する方法

GitHub Actionsから短縮リンクを作成・更新する方法を解説します。APIキーを暗号化されたシークレットとして保存し、動作するワークフロー、冪等なアップサート、最小権限のキーを使います。

Marius Voß
DevRel · edge infra
GitHub ActionsのURL短縮ステップをパイプラインとして描いた図。ワークフローの実行が暗号化されたシークレットを読み取り、スラッグを検索して、既存の短縮リンクを更新するか新しいリンクを作成する

GitHub ActionsのURL短縮ステップは、暗号化されたシークレットからAPIキーを読み取り、スラッグがすでに存在するかを確認し、存在すれば向き先を更新し、なければ作成するという数行のシェルです。プッシュのたびに実行すれば、同じ短縮リンクが常に最新のプレビュー、ドキュメントビルド、またはアーティファクトを指します。マーケットプレイスのアクションは必要ありません。curljqは、すべてのGitHubホストUbuntuランナーに搭載されています。

これが答えの全体であり、この記事の残りでは、数百回のワークフロー実行でもこれを確実に動かす方法を説明します。GitHub Actionsで短縮リンクを作成する方法を探す人は、通常は単一のPOSTリクエストまでたどり着きます。それは同じプルリクエストに2回目のプッシュをするまでは動きますが、その時点で作成リクエストが競合を返し、ジョブが失敗します。解決策は、このステップを作成処理ではなくアップサートとして扱うことです。もう一つ起きがちな問題はキーそのもので、CIジョブに必要な範囲をはるかに超える権限を持つ個人用トークンを使いがちです。

すでにリンクをコードとして管理しているなら、Terraformとしての短縮リンクは同じ考え方を宣言的に実現する方法で、人が決めたスケジュールで変更するリンクに適しています。ビルドが完了するまで向き先が存在しない場合は、ワークフローのステップが適しています。

GitHub ActionsのURL短縮ステップの仕組み

すべての実行では、https://api.elido.app/v1のREST APIに対して同じ3つの処理を行います。スラッグで絞り込んで、ワークスペースのリンクを一覧表示します。見つかったリンクにはPATCHを送り、何も見つからなければPOSTを送ります。短縮URLを$GITHUB_OUTPUTに書き込むため、次のステップで利用できます。

短縮サービスにランダムなスラッグを生成させてはいけないのはなぜでしょうか。そうすると、あとからリンクを見つけられないためです。スラッグは、ワークフローが実行のたびにすでに把握している値から作る必要があります。たとえば、プルリクエスト番号、ブランチ名、latestのような固定語です。安定したスラッグは安定した短縮URLを意味します。ブックマークするレビュアーや、チケットに貼り付けるプロダクトマネージャーにとって、そのことがすべての価値になります。

GitHub ActionsのURL短縮ステップの実行方法。ワークフローが暗号化されたシークレットからAPIキーを読み取り、スラッグでリンクを一覧表示し、スラッグが存在すればPATCH、存在しなければPOSTを送信してから、短縮URLをステップの出力に書き込む

APIキーを暗号化されたシークレットとして保存する

ダッシュボードでキーを作成し、一度だけコピーします(表示されるのはその一度だけで、elido_から始まります)。その後、ELIDO_API_KEYとして、Settings、Secrets and variables、Actionsの順に進んで保存します。GitHubのGitHub Actionsでシークレットを使う方法のガイドでは、リポジトリ、環境、組織の各レベルについて説明しています。デプロイを行うものなら、必須レビュアーを設定した環境に置きます。そうすれば、意図しないブランチから利用されることを防げます。

次の3つの値はシークレットではないため、設定変数に置いて、あとから読み取れるようにします。ELIDO_WORKSPACE_IDELIDO_DOMAIN_IDELIDO_HOSTです。作成リクエストにはドメインIDが必要なので重要です。GET /v1/workspaces/{workspace_id}/domainsを一度実行すれば確認できます。このリクエストは、各ドメインのidhostnameを返します。

シークレットは、ジョブ全体ではなくAPIを呼び出す1つのステップだけに割り当てます。ステップレベルのenvなら、作成したジョブが起動する他のすべてのプロセスからシークレットを隔離できます。自分で作成していないサードパーティーアクションも対象です。

すべてのプルリクエストでURLを短縮する実用的なワークフロー

これは、GitHubワークフローでURLを短縮する最も一般的な用途、つまりプルリクエストごとのプレビューリンクに使える完全なファイルです。.github/workflows/preview-link.ymlに配置し、プレビューのデプロイ先に合わせてDESTの行を変更してください。

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は、通常のcurl -sでは行われないエラーボディーの出力を保ったまま、4xxまたは5xxをステップの失敗にします。通常のcurl -sは401でも終了コード0を返し、ジョブが何事もなく続いてしまいます。リクエストボディーは文字列補間ではなくjq -nで構築しているため、引用符やアンパサンドを含む向き先でもJSONを壊せません。また、コメントステップはopenedの場合だけ実行されます。短縮URLは変わらないので、PRの存続期間中は1つのコメントが正しいままになり、プッシュのたびに通知が送られることもありません。

concurrencyブロックは飾りではありません。短時間に2回プッシュすると、設定しなければ2つの実行が開始され、どちらも「まだリンクがない」と判断して作成を試みます。ワークフローの同時実行を制御する方法についてはGitHubのドキュメントでグループ化の仕組みを説明しています。ここでは古い実行がキャンセルされるため、競合は起こりません。

冪等性: 短縮リンクを更新し、重複させない

冪等という言葉の下には、異なる2つの失敗が隠れています。このワークフローでは、それぞれを別に処理します。1つ目は再実行です。2回目のプッシュ、手動の「Re-run jobs」、再オープンしたPRなどが該当します。これには、検索してからPATCHする分岐を使います。2つ目は再試行されたリクエストです。curlがPOSTを送信した後、応答が届く前にネットワークが切断され、curlがもう一度送信するケースです。この場合はIdempotency-Keyヘッダーが対応します。Elidoはキーに対する最初の成功レスポンスを24時間保存し、一致する再試行にはそれを再生するため、作成は1回だけ実行されます。仕組みの詳細はレート制限、再試行、冪等性で説明しています。

GitHub Actionsで重複なく短縮リンクを作成する判断フロー。ワークスペース内でスラッグが完全一致すればPATCHに進み、一致しなければPOSTに進み、409の場合は共有ドメイン上の別のワークスペースがそのスラッグを所有していることを示す

スラッグの衝突は見落とされがちな部分です。スラッグはワークスペースごとではなく、リダイレクトドメインごとに一意です。共有ドメインでは、他のすべてのElido顧客が同じ名前空間に属しているため、pr-12のように単純なスラッグは誰かに取得されている可能性があります。検索では相手のリンクは見えません(自分のワークスペースだけを一覧表示するため)。そのためPOSTが送信され、409 slug already exists for this domainが返ります。解決策は2つあります。スラッグにプロジェクト名を加えるか、独自のカスタムドメインにCIリンクを置くことです。後者なら、名前空間を自分だけで使えます。私は両方を行います。

リポジトリ名をスラッグの先頭ではなく末尾に置く、もう一つの少し意外な理由もあります。qパラメーターは、スラッグ、向き先、タイトルに対する部分一致です。myapp-pr-1にすると、検索結果にはmyapp-pr-10からmyapp-pr-199まで含まれます。これは1ページで返される100件を超えるため、必要だったリンクが最も古いものとして末尾から落ちます。pr-1-myappなら、自分自身以外には一致しません。小さな点ですが、「なぜPR #1が何度も409になるのか」に気づくまで、私は恥ずかしいほど長い午後を費やしました。

自動化する価値のあるCI短縮リンクジョブ3選

プレビュー用ワークフローは1つのパターンです。トリガー、スラッグ、向き先を変えれば、チームが実際に自動化する処理の大半を同じステップでカバーできます(リリースノートは別のテーマで、リリースノート内のリンクを短縮する方法で扱っています)。

用途トリガースラッグステップの処理
PRごとのプレビュー用デプロイpull_requestpr-42-myappプッシュごとにアップサート、クローズ時に削除
ドキュメントのデプロイpush to maindocs-myappデプロイ直後のドキュメントURLにPATCH
最新のビルドpush to main or a taglatest-myapp保存したリンクIDに対してPATCH、検索なし
夜間アーティファクトschedulenightly-myapp最新のアーティファクトURLにPATCH

最新ビルドのケースは、これらの中で最も簡単です。手動で一度リンクを作成し、その数値IDを変数に保存すれば、ジョブは1回の呼び出しだけになります。

- 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のリダイレクトの比較では、詳しく説明しています。

プレビューリンクはPRがクローズしたときに整理します。トリガーの種類にclosedを追加し、検索処理を再利用して、DELETE /v1/workspaces/{workspace_id}/links/{link_id}を送信します。削除したスラッグは再利用できます。クリック履歴を残したい場合は、代わりに{"status": "disabled"}を指定してPATCHします。上記のアップサートは毎回status: "active"を設定するため、再オープンしたPRのリンクは復活します。

1つのリポジトリで試す準備ができましたか?無料のワークスペースを始めるからキーを作成し、3つの変数を設定すれば、上記のワークフローがそのまま実行されます。

CI用の最小権限APIキー

CIシークレットのキーは、ワークフローが行うことだけを実行できるようにし、それ以上の権限を与えないでください。個人用キーの仕組みを考えると、これは言うほど簡単ではありません。

個人用APIキーは、それを作成した人として認証されます。その人ができることはキーでもでき、その人が会社を辞めるとキーもそのアカウントに残ります。CIには、代わりにマシンユーザーを使います。これは1つのワークスペースに所属するサービスアカウントで、独自のロールを持ち、ログインした人間の管理者だけが発行または無効化できるトークンを保持します。ダッシュボードのMachine usersでエディターロールを付けて作成してください。これはリンクの作成、編集、削除が可能な組み込みロールの中で最も権限の低いものです。その後、有効期限を設定してトークンを発行します。マシンユーザーを無効にすれば、保持しているすべてのトークンを一度に無効化できます。シークレットが漏えいした日に必要なのは、まさにこのボタンです。

さらに、費用のかからない習慣が4つあります。

  • リポジトリごとに、その名前を付けたトークンを1つ使います。監査記録から、どのリポジトリがどのリンクを作成したか分かるようにするためです。
  • 依存するリンクを変更するワークフローには、必須レビュアーを設定した環境シークレットを使います。
  • 例のように、ワークフローの先頭でpermissions:を明示的に設定し、GITHUB_TOKENにはジョブが必要とする権限だけを与えます。
  • フォークのPRからシークレットにアクセスするためにpull_request_targetを使ってはいけません。GitHub Security Labの危険なpwnリクエストを防ぐ方法では、書き込みトークンの隣で信頼できないコードを実行すると危険な結果になる理由を説明しています。

ワークスペースでは、IP許可リストによってAPIアクセスを制限することもできます。固定された送信元を持つセルフホストランナーには強力な制御ですが、アドレスが大きく変動するプールから割り当てられるGitHubホストランナーには、ほとんど役に立ちません。ワークフローが本番環境に触れる場合は、GitHub公式の安全な利用に関するリファレンスに1時間かけて目を通す価値があります。

実際に起きる問題

ほとんどの失敗は4か所から発生し、--fail-with-bodyが有効なら、それぞれ読みやすいエラーとして現れます。すべての呼び出しで404になる場合は、通常、ワークスペースIDの変数が間違っているか、キーが別のワークスペースに属しています。domain_id is requiredという400は、変数が空であることを意味します。多くの場合、ジョブが使う環境とは別の環境に設定されています。409は、冪等性のセクションで説明した共有名前空間の衝突です。429はキーごとのレート制限を超えたことを意味します。実行ごとに1回のアップサートなら超えませんが、50個のジョブをマトリックスで実行すると超える可能性があります。

エラーではないものもあります。PATCHの後、しばらくは訪問者が古い向き先に到達する場合があります。リダイレクトは高速に動作するよう、訪問者の近くでキャッシュされるためです。更新直後に新しい向き先になったことを確認するスモークテストは、不安定になる可能性があります。短いバックオフを入れてポーリングするか、APIレスポンスに対して検証してください。

ステップの結果を外部に通知したい場合は、リンクが変更されたときに発火するリンクイベント用のWebhookと組み合わせてください。コミット前のローカルテストには、CLIガイドのcurlとjqのパターンも使えます。APIとSDKのリファレンスには、リンクエンドポイントが受け付けるすべてのフィールドを掲載しています。

コーナーストーン記事を読む → 短縮リンクをTerraformとして管理する

ブログ内の関連記事

よくある質問

GitHub Actionsで短縮リンクを作成できますか?

はい。ワークフローのステップから、curlを使って任意の短縮サービスのREST APIを呼び出せます。curlはjqとともにGitHubホストランナーにあらかじめインストールされています。このステップは暗号化されたシークレットからAPIキーを読み取り、向き先URLを送信し、結果として得られた短縮URLをステップの出力に書き込みます。後続のステップでは、そのURLをプルリクエストのコメントやジョブのサマリーに投稿できます。

GitHub ActionsにURL短縮サービスのAPIキーを保存するにはどうすればよいですか?

暗号化されたリポジトリシークレットまたは環境シークレットとして保存し、式の構文内でELIDO_API_KEY: secrets.ELIDO_API_KEYのようなenvエントリを使って、必要とする1つのステップにだけ割り当てます。GitHubはログ内の値をマスクします。ワークスペースIDやドメインIDのようなシークレットではない値は設定変数に置き、読み取れる状態にしておきます。

ワークフローを実行するたびに重複した短縮リンクが作成されるのを防ぐにはどうすればよいですか?

ステップをアップサートにします。プルリクエスト番号のような安定した値からスラッグを導出し、最初に検索します。すでに存在する場合はPATCHを送信して向き先を変更し、検索結果が空の場合だけ作成します。作成リクエストにIdempotency-Keyヘッダーを付ければ、ネットワークタイムアウト後にリクエストを再試行する別のケースにも対応できます。

短縮リンクを作成するときにワークフローが409を返すのはなぜですか?

そのドメインではスラッグがすでに使われています。Elidoではスラッグはリダイレクトドメインごとに一意であり、共有ドメインは他のすべてのワークスペースと共有されるため、pr-12のような一般的なスラッグはすでに存在する可能性があります。スラッグにプロジェクトの接頭辞または接尾辞を追加するか、名前空間全体を自分で管理できる独自のカスタムドメインを使ってください。

フォークからのプルリクエストでも短縮リンクのステップは動作しますか?

通常のpull_requestトリガーでは動作しません。GitHubは、フォークから開始されたワークフローにリポジトリシークレットを渡さないためです。headリポジトリに対するif条件を使って、フォークの場合はジョブをスキップしてください。シークレットを取得するためにpull_request_targetへ切り替えるのは危険です。レビューしていないコードの隣で書き込み権限を持って実行されるためです。

最新のビルドを指すリンクには301と302のどちらのリダイレクトを使うべきですか?

302または307を使ってください。ブラウザーは301を無期限にキャッシュできるため、ワークフローがリンクを移動した後も、再訪問者は古いビルドに到達し続ける可能性があります。Elidoのリンクはredirect_statusを設定しない場合、デフォルトで302になります。パイプラインが向き先を変更するリンクには、これが適切な選択です。

Elidoを試す

URLを貼り付けて短縮リンクを取得

登録不要。リンクは30日間有効。永久に保存するには登録してください。

Free、登録不要 · 1日あたり2件

Elidoを試す

EUホスティングのURL短縮サービス。カスタムドメイン、詳細な分析、オープンAPI付き。無料プラン - クレジットカード不要。

タグ
github actions url shortener
create short link in github actions
shorten url github workflow
preview deployments
ci/cd
api keys

続きを読む