process-upload/action.yml

237 lines
6.6 KiB
YAML

name: Process upload
description: Move or normalize one uploaded file, remove the source, and optionally commit and push the result
inputs:
source-dir:
description: Directory containing pending uploads
required: false
default: ingest
file-pattern:
description: Filename pattern passed to find, such as *.csv or *.xlsx
required: false
default: "*"
source-file:
description: >
Optional exact repository-relative source path. If omitted, exactly one
matching file must be present directly under source-dir.
required: false
default: ""
target-file:
description: Repository-relative destination path
required: true
fetch-command:
description: >
Optional shell command to materialize INPUT_FILE before processing.
Useful for git-annex-backed files.
required: false
default: ""
normalizer:
description: >
Optional shell command. It must read INPUT_FILE and write OUTPUT_FILE.
If empty, the input is moved unchanged.
required: false
default: ""
commit-and-push:
description: Commit and push the resulting worktree changes
required: false
default: "true"
commit-message:
description: Commit subject
required: false
default: "chore: process uploaded file"
commit-user-name:
description: Git commit author name
required: false
default: "Workflow runner"
commit-user-email:
description: Optional Git commit author email
required: false
default: ""
outputs:
source-file:
description: The processed source path
value: ${{ steps.locate.outputs.source-file }}
target-file:
description: The generated target path
value: ${{ steps.locate.outputs.target-file }}
runs:
using: composite
steps:
- name: Locate upload
id: locate
shell: bash
env:
SOURCE_DIR: ${{ inputs.source-dir }}
FILE_PATTERN: ${{ inputs.file-pattern }}
SOURCE_FILE: ${{ inputs.source-file }}
TARGET_FILE: ${{ inputs.target-file }}
run: |
set -euo pipefail
if [[ "$SOURCE_DIR" = /* || "$TARGET_FILE" = /* ]]; then
echo "source-dir and target-file must be repository-relative paths." >&2
exit 1
fi
case "$SOURCE_DIR/$TARGET_FILE" in
*/../*|../*|*/..|..)
echo "Path traversal is not allowed." >&2
exit 1
;;
esac
if [[ -n "$SOURCE_FILE" ]]; then
case "$SOURCE_FILE" in
/*|../*|*/../*|..)
echo "Invalid source-file path." >&2
exit 1
;;
esac
if [[ ! -f "$SOURCE_FILE" && ! -L "$SOURCE_FILE" ]]; then
echo "Source file does not exist: $SOURCE_FILE" >&2
exit 1
fi
source="$SOURCE_FILE"
else
if [[ ! -d "$SOURCE_DIR" ]]; then
echo "No upload directory exists: $SOURCE_DIR"
exit 0
fi
mapfile -d '' files < <(
find -- "$SOURCE_DIR" \
-maxdepth 1 \
\( -type f -o -type l \) \
-name "$FILE_PATTERN" \
! -name '.gitkeep' \
! -name '.gitignore' \
-print0
)
case "${#files[@]}" in
0)
echo "No matching upload found in $SOURCE_DIR."
exit 0
;;
1)
source="${files[0]}"
;;
*)
echo "More than one upload was found in $SOURCE_DIR:" >&2
printf ' %q\n' "${files[@]}" >&2
exit 1
;;
esac
fi
if [[ "$source" == "$TARGET_FILE" ]]; then
echo "Source and target must be different." >&2
exit 1
fi
{
printf 'source-file=%s\n' "$source"
printf 'target-file=%s\n' "$TARGET_FILE"
} >> "$GITHUB_OUTPUT"
- name: Fetch content
if: ${{ inputs.fetch-command != '' && steps.locate.outputs.source-file != '' }}
shell: bash
env:
INPUT_FILE: ${{ steps.locate.outputs.source-file }}
OUTPUT_FILE: ${{ steps.locate.outputs.target-file }}
FETCH_COMMAND: ${{ inputs.fetch-command }}
run: |
set -euo pipefail
# FETCH_COMMAND is workflow configuration. The uploaded filename is
# passed through the quoted INPUT_FILE environment variable.
bash -c "$FETCH_COMMAND"
- name: Move upload unchanged
if: ${{ inputs.normalizer == '' && steps.locate.outputs.source-file != '' }}
shell: bash
env:
INPUT_FILE: ${{ steps.locate.outputs.source-file }}
OUTPUT_FILE: ${{ steps.locate.outputs.target-file }}
run: |
set -euo pipefail
mkdir -p -- "$(dirname -- "$OUTPUT_FILE")"
mv -f -- "$INPUT_FILE" "$OUTPUT_FILE"
- name: Normalize upload
if: ${{ inputs.normalizer != '' && steps.locate.outputs.source-file != '' }}
shell: bash
env:
INPUT_FILE: ${{ steps.locate.outputs.source-file }}
OUTPUT_FILE: ${{ steps.locate.outputs.target-file }}
NORMALIZER: ${{ inputs.normalizer }}
run: |
set -euo pipefail
mkdir -p -- "$(dirname -- "$OUTPUT_FILE")"
rm -f -- "$OUTPUT_FILE"
# NORMALIZER is trusted workflow configuration. It must read
# "$INPUT_FILE" and write "$OUTPUT_FILE".
bash -c "$NORMALIZER"
if [[ ! -f "$OUTPUT_FILE" && ! -L "$OUTPUT_FILE" ]]; then
echo "Normalizer did not create $OUTPUT_FILE." >&2
exit 1
fi
# The original upload is removed only after successful normalization.
rm -f -- "$INPUT_FILE"
- name: Commit and push changes
if: ${{ inputs.commit-and-push == 'true' && steps.locate.outputs.source-file != '' }}
shell: bash
env:
INPUT_FILE: ${{ steps.locate.outputs.source-file }}
COMMIT_MESSAGE: ${{ inputs.commit-message }}
COMMIT_USER_NAME: ${{ inputs.commit-user-name }}
COMMIT_USER_EMAIL: ${{ inputs.commit-user-email }}
ACTOR: ${{ forgejo.actor }}
SERVER_URL: ${{ forgejo.server_url }}
run: |
set -euo pipefail
git add -A
if git diff --cached --quiet; then
echo "No changes to commit."
exit 0
fi
if [[ -z "$COMMIT_USER_EMAIL" ]]; then
server_host="${SERVER_URL#*://}"
COMMIT_USER_EMAIL="${ACTOR}@${server_host}"
fi
git config user.name "$COMMIT_USER_NAME"
git config user.email "$COMMIT_USER_EMAIL"
git commit \
-m "$COMMIT_MESSAGE" \
-m "source: $INPUT_FILE"
git push