Initial commit of reusable Portainer action

This commit is contained in:
Bot
2026-05-29 22:29:54 +00:00
commit b7ea4f8385
3 changed files with 162 additions and 0 deletions

45
README.md Normal file
View File

@@ -0,0 +1,45 @@
# Portainer Stack Deploy Action
A reusable Gitea/GitHub action to automatically create or update a Docker Compose Stack in Portainer using the Portainer API.
This action is idempotent: if the stack doesn't exist, it creates it. If it exists, it updates it and pulls the latest images.
## Usage
In your repository, create your `.gitea/workflows/deploy.yaml` and reference this action:
```yaml
name: Deploy to Portainer
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
# ... (Your Docker Build and Push steps here) ...
- name: Deploy Stack to Portainer
uses: kaykayyali/portainer-deploy-action@main
with:
portainer_url: 'http://portainer.home.local:9000' # Your Portainer URL
portainer_token: ${{ secrets.PORTAINER_API_TOKEN }}
stack_name: 'my-awesome-app'
compose_file: 'docker-compose.yml' # Path to your compose file in the repo
```
## Inputs
| Name | Required | Default | Description |
| --- | --- | --- | --- |
| `portainer_url` | Yes | | The full URL to your Portainer instance API |
| `portainer_token` | Yes | | Your Portainer API Token (Access Token) |
| `stack_name` | Yes | | The name you want to give the stack in Portainer |
| `endpoint_id` | No | `1` | The Portainer Environment ID (Usually 1 for local) |
| `compose_file` | No | `docker-compose.yml` | The relative path to your docker-compose file |

33
action.yml Normal file
View File

@@ -0,0 +1,33 @@
name: 'Portainer Stack Deploy'
description: 'Deploys or updates a Docker Compose stack via the Portainer API'
inputs:
portainer_url:
description: 'URL to Portainer instance (e.g., http://portainer:9000)'
required: true
portainer_token:
description: 'Portainer API Token'
required: true
endpoint_id:
description: 'Portainer Environment/Endpoint ID (default: 1)'
required: false
default: '1'
stack_name:
description: 'Name of the stack to create or update'
required: true
compose_file:
description: 'Path to docker-compose.yml file'
required: false
default: 'docker-compose.yml'
runs:
using: "composite"
steps:
- name: Deploy to Portainer
shell: bash
env:
PORTAINER_URL: ${{ inputs.portainer_url }}
PORTAINER_TOKEN: ${{ inputs.portainer_token }}
ENDPOINT_ID: ${{ inputs.endpoint_id }}
STACK_NAME: ${{ inputs.stack_name }}
COMPOSE_FILE: ${{ inputs.compose_file }}
run: ${{ github.action_path }}/deploy.sh

84
deploy.sh Executable file
View File

@@ -0,0 +1,84 @@
#!/bin/bash
set -e
# Ensure inputs are present
if [ -z "$PORTAINER_URL" ] || [ -z "$PORTAINER_TOKEN" ] || [ -z "$STACK_NAME" ]; then
echo "Error: PORTAINER_URL, PORTAINER_TOKEN, and STACK_NAME must be set."
exit 1
fi
if [ ! -f "$COMPOSE_FILE" ]; then
echo "Error: Compose file $COMPOSE_FILE not found."
exit 1
fi
echo "Deploying Stack: $STACK_NAME"
echo "Portainer URL: $PORTAINER_URL"
echo "Endpoint ID: $ENDPOINT_ID"
# 1. Check if stack exists
echo "Checking if stack exists..."
RESPONSE=$(curl -s -w "\n%{http_code}" -H "X-API-Key: $PORTAINER_TOKEN" "$PORTAINER_URL/api/stacks")
HTTP_STATUS=$(tail -n1 <<< "$RESPONSE")
BODY=$(sed '$ d' <<< "$RESPONSE")
if [ "$HTTP_STATUS" -ne 200 ]; then
echo "Failed to fetch stacks. HTTP Status: $HTTP_STATUS"
echo "$BODY"
exit 1
fi
# Try to extract the ID for the matching stack name
STACK_ID=$(echo "$BODY" | jq -r ".[] | select(.Name == \"$STACK_NAME\") | .Id")
COMPOSE_CONTENT=$(cat "$COMPOSE_FILE")
# Escape content for JSON payload
COMPOSE_JSON=$(jq -Rs . <<< "$COMPOSE_CONTENT")
if [ -n "$STACK_ID" ] && [ "$STACK_ID" != "null" ]; then
echo "Stack '$STACK_NAME' exists with ID: $STACK_ID. Updating..."
# PUT to update stack
# pullImage=true ensures it pulls the latest image if tag is the same
PAYLOAD="{\"stackFileContent\": $COMPOSE_JSON, \"env\": [], \"prune\": true, \"pullImage\": true}"
UPDATE_RES=$(curl -s -w "\n%{http_code}" -X PUT \
-H "X-API-Key: $PORTAINER_TOKEN" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
"$PORTAINER_URL/api/stacks/$STACK_ID?endpointId=$ENDPOINT_ID")
U_STATUS=$(tail -n1 <<< "$UPDATE_RES")
U_BODY=$(sed '$ d' <<< "$UPDATE_RES")
if [ "$U_STATUS" -eq 200 ]; then
echo "Stack successfully updated!"
else
echo "Failed to update stack. HTTP $U_STATUS"
echo "$U_BODY"
exit 1
fi
else
echo "Stack '$STACK_NAME' does not exist. Creating..."
# POST to create stack
PAYLOAD="{\"name\": \"$STACK_NAME\", \"stackFileContent\": $COMPOSE_JSON, \"env\": []}"
CREATE_RES=$(curl -s -w "\n%{http_code}" -X POST \
-H "X-API-Key: $PORTAINER_TOKEN" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
"$PORTAINER_URL/api/stacks/create/standalone/string?endpointId=$ENDPOINT_ID")
C_STATUS=$(tail -n1 <<< "$CREATE_RES")
C_BODY=$(sed '$ d' <<< "$CREATE_RES")
if [ "$C_STATUS" -eq 200 ]; then
echo "Stack successfully created!"
else
echo "Failed to create stack. HTTP $C_STATUS"
echo "$C_BODY"
exit 1
fi
fi