diff --git a/.github/workflows/test-local.yml b/.github/workflows/test-local.yml new file mode 100644 index 0000000..608f395 --- /dev/null +++ b/.github/workflows/test-local.yml @@ -0,0 +1,48 @@ +name: Test (local) + +on: + workflow_dispatch: + inputs: + shipyrd-url: + description: "Base URL of your local Shipyrd instance." + required: true + +jobs: + test-pre-deploy: + name: Test pre-deploy notification + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run action (pre-deploy) + uses: ./ + with: + api-token: ${{ secrets.SHIPYRD_TOKEN }} + shipyrd-url: ${{ inputs.shipyrd-url }} + status: pre-deploy + destination: staging + + test-post-deploy: + name: Test post-deploy notification + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run action (post-deploy, default status) + uses: ./ + with: + api-token: ${{ secrets.SHIPYRD_TOKEN }} + shipyrd-url: ${{ inputs.shipyrd-url }} + + test-failed: + name: Test failed notification + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run action (failed) + uses: ./ + with: + api-token: ${{ secrets.SHIPYRD_TOKEN }} + shipyrd-url: ${{ inputs.shipyrd-url }} + status: failed diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6884613 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,104 @@ +name: Test + +on: + push: + pull_request: + +jobs: + test-pre-deploy: + name: Test pre-deploy notification + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Start mock server + shell: bash + run: | + while true; do + echo -e "HTTP/1.1 201 Created\r\nContent-Type: application/json\r\nContent-Length: 2\r\n\r\n{}" \ + | nc -l 8080 -q 1 + done & + sleep 0.5 + + - name: Run action (pre-deploy) + uses: ./ + with: + api-token: test-token + shipyrd-url: http://localhost:8080 + status: pre-deploy + destination: staging + + test-post-deploy: + name: Test post-deploy notification + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Start mock server + shell: bash + run: | + while true; do + echo -e "HTTP/1.1 201 Created\r\nContent-Type: application/json\r\nContent-Length: 2\r\n\r\n{}" \ + | nc -l 8080 -q 1 + done & + sleep 0.5 + + - name: Run action (post-deploy, default status) + uses: ./ + with: + api-token: test-token + shipyrd-url: http://localhost:8080 + + test-failed: + name: Test failed notification + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Start mock server + shell: bash + run: | + while true; do + echo -e "HTTP/1.1 201 Created\r\nContent-Type: application/json\r\nContent-Length: 2\r\n\r\n{}" \ + | nc -l 8080 -q 1 + done & + sleep 0.5 + + - name: Run action (failed) + uses: ./ + with: + api-token: test-token + shipyrd-url: http://localhost:8080 + status: failed + + test-auth-failure: + name: Test auth failure exits non-zero + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Start mock server (returns 401) + shell: bash + run: | + while true; do + echo -e "HTTP/1.1 401 Unauthorized\r\nContent-Type: application/json\r\nContent-Length: 24\r\n\r\n{\"error\":\"Invalid token\"}" \ + | nc -l 8080 -q 1 + done & + sleep 0.5 + + - name: Run action (should fail) + id: notify + uses: ./ + continue-on-error: true + with: + api-token: bad-token + shipyrd-url: http://localhost:8080 + + - name: Verify step failed + shell: bash + run: | + if [ "${{ steps.notify.outcome }}" != "failure" ]; then + echo "Expected action to fail on 401 but it succeeded" + exit 1 + fi + echo "Correctly failed on 401" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..58ae64b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Shipyrd + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index bb1eb79..9865780 100644 --- a/README.md +++ b/README.md @@ -1 +1,80 @@ -# github-deploy-action +# Shipyrd Deploy Action + +Notify [Shipyrd](https://shipyrd.io) of deploy lifecycle events from your GitHub Actions workflow. + +## Usage + +Add three steps around your deploy — one before, one on success, one on failure: + +```yaml +steps: + - uses: actions/checkout@v4 + + - name: Notify Shipyrd (pre-deploy) + uses: shipyrd/deploy-action@v1 + with: + api-token: ${{ secrets.SHIPYRD_API_KEY }} + status: pre-deploy + + # --- your deploy steps --- + + - name: Notify Shipyrd (post-deploy) + if: success() + uses: shipyrd/deploy-action@v1 + with: + api-token: ${{ secrets.SHIPYRD_API_KEY }} + # status defaults to post-deploy + + - name: Notify Shipyrd (failed) + if: failure() + uses: shipyrd/deploy-action@v1 + with: + api-token: ${{ secrets.SHIPYRD_API_KEY }} + status: failed +``` + +## Setup + +1. Go to your application's **Setup** page in Shipyrd and copy the deploy token. +2. In your GitHub repository, go to **Settings → Secrets and variables → Actions** and add a secret named `SHIPYRD_API_KEY` with that token as the value. + +## Inputs + +| Input | Required | Default | Description | +|---|---|---|---| +| `api-token` | yes | — | Your Shipyrd deploy token | +| `status` | no | `post-deploy` | `pre-deploy`, `post-deploy`, or `failed` | +| `shipyrd-url` | no | `https://hooks.shipyrd.io` | Override for self-hosted Shipyrd | +| `service` | no | — | Optional service name included in `service_version` (e.g. `my-app@sha`). The application is identified by the API token. | +| `destination` | no | `production` | Deploy destination (e.g. `staging`) | +| `performer` | no | `github.actor` | Who triggered the deploy | +| `version` | no | `github.sha` | Git SHA being deployed | +| `commit-message` | no | head commit message | Commit message for this deploy | + +## Why `if: success()` and `if: failure()`? + +Without these conditions, a failed deploy step would skip the `post-deploy` and `failed` notifications entirely, leaving the Shipyrd dashboard showing the deploy permanently stuck in `pre-deploy`. The `success()`/`failure()` split ensures Shipyrd always receives a terminal status. + +## Testing locally + +Use [act](https://github.com/nektos/act) to run the test workflow against a local Shipyrd instance: + +```bash +act workflow_dispatch -W .github/workflows/test-local.yml \ + -j test-post-deploy \ + -s SHIPYRD_TOKEN=your-token \ + --input shipyrd-url=http://host.docker.internal:3000 \ + --env COMMIT_MESSAGE="your commit message" +``` + +`host.docker.internal` routes to your Mac's localhost from inside the Docker container that `act` uses. Replace the port with whatever your local Shipyrd instance is running on. + +## Releasing + +From an up-to-date `main` branch: + +```bash +bin/release 1.0.0 +``` + +This creates a semver tag (`v1.0.0`) and updates the major version tag (`v1`) so users pinned to `@v1` get the latest. diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..34c1e60 --- /dev/null +++ b/action.yml @@ -0,0 +1,93 @@ +name: "Shipyrd Deploy Notification" +description: "Notify Shipyrd of deploy lifecycle events from your GitHub Actions workflow." +author: "Shipyrd" + +branding: + icon: "send" + color: "blue" + +inputs: + api-token: + description: "Your Shipyrd API token (from Application Settings). Store this as a GitHub Actions secret." + required: true + + shipyrd-url: + description: "Base URL of your Shipyrd instance. Override if self-hosted." + required: false + default: "https://hooks.shipyrd.io" + + status: + description: "Deploy lifecycle status: 'pre-deploy', 'post-deploy', or 'failed'." + required: false + default: "post-deploy" + + service: + description: "The service name to include in the service_version field (e.g. my-app). Optional — the application is identified by the API token." + required: false + default: "" + + destination: + description: "The deploy destination (e.g. production, staging)." + required: false + default: "production" + + performer: + description: "The person or identity performing the deploy." + required: false + default: "${{ github.actor }}" + + version: + description: "The git SHA being deployed." + required: false + default: "${{ github.sha }}" + + commit-message: + description: "The commit message for this deploy." + required: false + default: "${{ github.event.head_commit.message }}" + +runs: + using: "composite" + steps: + - name: Notify Shipyrd + shell: bash + env: + SHIPYRD_TOKEN: ${{ inputs.api-token }} + SHIPYRD_URL: ${{ inputs.shipyrd-url }} + DEPLOY_STATUS: ${{ inputs.status }} + DEPLOY_SERVICE: ${{ inputs.service }} + DEPLOY_DESTINATION: ${{ inputs.destination }} + DEPLOY_PERFORMER: ${{ inputs.performer }} + DEPLOY_VERSION: ${{ inputs.version }} + DEPLOY_COMMIT_MESSAGE: ${{ inputs.commit-message || env.COMMIT_MESSAGE }} + run: | + RECORDED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ) + SERVICE_VERSION="${DEPLOY_SERVICE:+${DEPLOY_SERVICE}@}${DEPLOY_VERSION}" + + PAYLOAD=$(jq -n \ + --arg status "$DEPLOY_STATUS" \ + --arg performer "$DEPLOY_PERFORMER" \ + --arg version "$DEPLOY_VERSION" \ + --arg commit_message "$DEPLOY_COMMIT_MESSAGE" \ + --arg service_version "$SERVICE_VERSION" \ + --arg destination "$DEPLOY_DESTINATION" \ + --arg recorded_at "$RECORDED_AT" \ + '{deploy: {status: $status, performer: $performer, version: $version, commit_message: $commit_message, service_version: $service_version, destination: $destination, command: "deploy", recorded_at: $recorded_at}}') + + HTTP_STATUS=$(curl -s -o /tmp/shipyrd_response.json -w "%{http_code}" \ + -X POST "${SHIPYRD_URL}/deploys.json" \ + -H "Authorization: Bearer ${SHIPYRD_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD") + + if [ "$HTTP_STATUS" -eq 201 ]; then + echo "Shipyrd notified (${DEPLOY_STATUS} for ${SERVICE_VERSION} → ${DEPLOY_DESTINATION})" + elif [ "$HTTP_STATUS" -eq 401 ]; then + echo "::error::Shipyrd authentication failed. Check that your api-token secret is set correctly." + exit 1 + elif [ "$HTTP_STATUS" -eq 422 ]; then + echo "::error::Shipyrd rejected the notification: $(cat /tmp/shipyrd_response.json)" + exit 1 + else + echo "::warning::Shipyrd returned unexpected status ${HTTP_STATUS}: $(cat /tmp/shipyrd_response.json)" + fi diff --git a/bin/release b/bin/release new file mode 100755 index 0000000..fa45b0a --- /dev/null +++ b/bin/release @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ $# -lt 1 ]; then + echo "Usage: bin/release " + echo "Example: bin/release 1.2.0" + exit 1 +fi + +VERSION="$1" +TAG="v${VERSION}" +MAJOR_TAG="v$(echo "$VERSION" | cut -d. -f1)" + +# Ensure we're on main and up to date +CURRENT_BRANCH=$(git branch --show-current) +if [ "$CURRENT_BRANCH" != "main" ]; then + echo "Error: must be on main branch (currently on $CURRENT_BRANCH)" + exit 1 +fi + +git fetch origin main +if [ "$(git rev-parse HEAD)" != "$(git rev-parse origin/main)" ]; then + echo "Error: local main is not up to date with origin/main" + exit 1 +fi + +# Ensure tag doesn't already exist +if git rev-parse "$TAG" >/dev/null 2>&1; then + echo "Error: tag $TAG already exists" + exit 1 +fi + +echo "Releasing $TAG (major tag: $MAJOR_TAG)" + +# Create version tag and update major version tag +git tag "$TAG" +git tag -f "$MAJOR_TAG" + +# Push tags +git push origin "$TAG" +git push origin "$MAJOR_TAG" --force + +echo "Done! Tags $TAG and $MAJOR_TAG pushed to origin." +echo "Create a GitHub release at: https://github.com/Shipyrd/github-deploy-action/releases/new?tag=$TAG"