Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b860fbc27 | |||
| 55b2e0f545 | |||
| adbbaf54a4 | |||
| a991512d79 | |||
| 3b21441524 | |||
| ec5cd5712e | |||
| ea182577d7 | |||
| 37d938f5f0 | |||
| 929cf8419e | |||
| 87ee3d4c0f | |||
| e6c26f9759 | |||
| b0957a39c2 | |||
| 863746b65d | |||
| 2c09b5c349 | |||
| 749faf7c46 | |||
| 12df1398a2 | |||
| e50b1d8f89 | |||
| 370c2967f0 | |||
| 4ca5287d67 | |||
| d35a5cc717 | |||
| 47eb32cd7a | |||
| 28971b4b3b | |||
| 7c6196dbb5 | |||
| 3de900eeb5 | |||
| 0147569aa9 | |||
| 3bc16776cc | |||
| 2603f780e6 | |||
| 72fcf2f74f |
27
.gitea/workflows/codex.yml
Normal file
27
.gitea/workflows/codex.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Codex Reply
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
codex:
|
||||
runs-on: linux_amd64
|
||||
if: |
|
||||
contains(github.event.comment.body, '@codex') &&
|
||||
github.event.comment.user.login != 'codex'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: ./install-opencode
|
||||
|
||||
- uses: ./codex-reply
|
||||
with:
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
repository: ${{ github.repository }}
|
||||
comment-body: ${{ github.event.comment.body }}
|
||||
comment-author: ${{ github.event.comment.user.login }}
|
||||
issue-title: ${{ github.event.issue.title }}
|
||||
issue-body: ${{ github.event.issue.body }}
|
||||
api-token: ${{ secrets.TOKEN }}
|
||||
api-key: ${{ secrets.OPENCODE_API_KEY }}
|
||||
22
.gitea/workflows/triage-issue.yml
Normal file
22
.gitea/workflows/triage-issue.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
name: Triage Issue
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: linux_amd64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: ./install-opencode
|
||||
|
||||
- uses: ./triage-issue
|
||||
with:
|
||||
issue-title: ${{ github.event.issue.title }}
|
||||
issue-body: ${{ github.event.issue.body }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
repository: ${{ github.repository }}
|
||||
api-token: ${{ secrets.TOKEN }}
|
||||
api-key: ${{ secrets.OPENCODE_API_KEY }}
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.idea/
|
||||
.ai/
|
||||
153
README.md
153
README.md
@@ -0,0 +1,153 @@
|
||||
# Gitea Actions
|
||||
|
||||
Repository contenente action [Gitea Actions](https://docs.gitea.com/usage/actions/overview) centralizzate
|
||||
utilizzabili da altri repository del workspace.
|
||||
|
||||
## Action disponibili
|
||||
|
||||
### `install-opencode`
|
||||
|
||||
Installa [OpenCode](https://opencode.ai) globalmente via npm. Verifica la presenza di Node.js e npm,
|
||||
installando npm automaticamente su Alpine se mancante.
|
||||
|
||||
```yaml
|
||||
- uses: https://<host>/<owner>/Actions/install-opencode@<ref>
|
||||
# with:
|
||||
# version: "1.15.0" # fissa una versione specifica
|
||||
```
|
||||
|
||||
### `opencode-prompt`
|
||||
|
||||
Esegue un prompt OpenCode sul codice del repository. Richiede che `install-opencode`
|
||||
sia eseguito prima, oppure che OpenCode sia già presente sul runner.
|
||||
|
||||
```yaml
|
||||
- id: ai
|
||||
uses: https://<host>/<owner>/Actions/opencode-prompt@<ref>
|
||||
with:
|
||||
prompt: "Analizza il codice e trova potenziali bug"
|
||||
api-key: ${{ secrets.OPENCODE_API_KEY }}
|
||||
# opzionali:
|
||||
model: "deepseek/deepseek-v4-flash" # default
|
||||
agent: "" # agente opencode
|
||||
working-directory: "" # default: radice repo
|
||||
```
|
||||
|
||||
L'output `result` contiene la risposta di OpenCode e viene salvato anche in
|
||||
`$GITHUB_WORKSPACE/opencode-output.txt`.
|
||||
|
||||
### `triage-issue`
|
||||
|
||||
Analizza automaticamente le issue in apertura con OpenCode: classifica come **bug** o
|
||||
**richiesta**, produce un riassunto, una gap analisi rispetto al codice sorgente, domande
|
||||
aperte e un report tecnico dettagliato in formato Markdown caricato come asset.
|
||||
|
||||
#### Utilizzo
|
||||
|
||||
```yaml
|
||||
- uses: https://<host>/<owner>/Actions/triage-issue@<ref>
|
||||
with:
|
||||
issue-title: ${{ github.event.issue.title }}
|
||||
issue-body: ${{ github.event.issue.body }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
repository: ${{ github.repository }}
|
||||
api-token: ${{ secrets.TOKEN }}
|
||||
api-key: ${{ secrets.OPENCODE_API_KEY }}
|
||||
# opzionali:
|
||||
model: "deepseek/deepseek-v4-flash" # default
|
||||
gitea-host: "https://git.incloud.ovh" # default
|
||||
```
|
||||
|
||||
#### Prerequisiti nel repository target
|
||||
|
||||
1. **Secrets**: `TOKEN` (API Gitea), `OPENCODE_API_KEY` (API AI)
|
||||
2. **Label**: `bug` (rosso `dc3545`) e `richiesta` (blu `007bff`)
|
||||
3. **Workflow** (esempio `.gitea/workflows/triage.yml`):
|
||||
|
||||
```yaml
|
||||
name: Triage Issue
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: linux_amd64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: https://git.incloud.ovh/anuti/Actions/install-opencode@main
|
||||
- uses: https://git.incloud.ovh/anuti/Actions/triage-issue@main
|
||||
with:
|
||||
issue-title: ${{ github.event.issue.title }}
|
||||
issue-body: ${{ github.event.issue.body }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
repository: ${{ github.repository }}
|
||||
api-token: ${{ secrets.TOKEN }}
|
||||
api-key: ${{ secrets.OPENCODE_API_KEY }}
|
||||
```
|
||||
|
||||
#### Output
|
||||
|
||||
- **Label** `bug` o `richiesta` applicata sull'issue
|
||||
- **Commento** con riassunto, gap analisi e domande aperte
|
||||
- **Asset Markdown** (`triage-issue-N.md`) con analisi tecnica completa, linkato nel commento
|
||||
|
||||
---
|
||||
|
||||
### `version-from-tag`
|
||||
|
||||
Estrae la versione da un tag (formato `v1.2.3.4[-suffix]`) e produce le variabili
|
||||
`appver`, `fullver`, `suffix` e `version`.
|
||||
|
||||
```yaml
|
||||
- name: Calcola versione
|
||||
uses: https://<host>/<owner>/Actions/version-from-tag@<ref>
|
||||
with:
|
||||
ref-name: ${{ github.ref_name }}
|
||||
```
|
||||
|
||||
### `publish-dotnet`
|
||||
|
||||
Compila (restore + publish) un progetto .NET e sincronizza l'output su un path
|
||||
locale via `rsync`.
|
||||
|
||||
```yaml
|
||||
- name: Publish
|
||||
uses: https://<host>/<owner>/Actions/publish-dotnet@<ref>
|
||||
with:
|
||||
project: src/MyApp/MyApp.csproj
|
||||
output-path: /var/publish/myapp
|
||||
version: ${{ steps.versione.outputs.appver }}
|
||||
# opzionali:
|
||||
configuration: Release
|
||||
subpath: "wwwroot"
|
||||
exclude-dirs: store
|
||||
exclude-files: appsettings.json
|
||||
```
|
||||
|
||||
### `deploy-iis`
|
||||
|
||||
Esegue il deploy su IIS: ferma sito/application pool, copia i file via `robocopy`,
|
||||
riavvia i servizi.
|
||||
|
||||
```yaml
|
||||
- name: Deploy IIS
|
||||
uses: https://<host>/<owner>/Actions/deploy-iis@<ref>
|
||||
with:
|
||||
source-path: /var/publish/myapp
|
||||
destination-path: C:\inetpub\wwwroot\myapp
|
||||
site-name: MySite
|
||||
app-pool-name: MyAppPool
|
||||
exclude-dirs: store
|
||||
exclude-files: appsettings.json
|
||||
```
|
||||
|
||||
## Versionamento delle action
|
||||
|
||||
Per puntare a una versione stabile, crea un tag su questo repository
|
||||
(es. `v1.0.0`) e usalo nel riferimento:
|
||||
|
||||
```yaml
|
||||
uses: https://<host>/<owner>/Actions/publish-dotnet@v1.0.0
|
||||
```
|
||||
|
||||
Oppure punta a un branch (`@main`) per avere sempre l'ultima versione.
|
||||
|
||||
90
SETUP_TRIAGE.md
Normal file
90
SETUP_TRIAGE.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Setup triage automatico su un repository
|
||||
|
||||
## Prerequisiti
|
||||
|
||||
- Gitea Actions abilitate sul repository
|
||||
- Issues abilitate sul repository
|
||||
- Runner disponibile con label `linux_amd64` (o label equivalente)
|
||||
- Accesso admin al repository per creare secrets e labels
|
||||
|
||||
## Passaggi
|
||||
|
||||
### 1. Secrets
|
||||
|
||||
Crea i seguenti secrets nel repository (`Settings → Actions → Secrets`):
|
||||
|
||||
| Secret | Valore | Descrizione |
|
||||
|---|---|---|
|
||||
| `TOKEN` | Token API Gitea | Permette all'action di applicare label e commentare |
|
||||
| `OPENCODE_API_KEY` | API key OpenCode | Autenticazione per il provider AI |
|
||||
|
||||
### 2. Labels
|
||||
|
||||
Crea le label `bug` e `richiesta` (o verifica che esistano):
|
||||
|
||||
```
|
||||
bug #ee0701 Malfunzionamento, errore, crash o anomalia
|
||||
richiesta #007bff Nuova funzionalità, miglioramento o ottimizzazione
|
||||
```
|
||||
|
||||
Se il repo ha già label simili con nomi diversi, unificarle sulla versione minuscola per evitare ambiguità col triage.
|
||||
|
||||
### 3. Workflow
|
||||
|
||||
Crea il file `.gitea/workflows/triage-issue.yml`:
|
||||
|
||||
```yaml
|
||||
name: Triage Issue
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: linux_amd64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: https://git.incloud.ovh/anuti/Actions/install-opencode@main
|
||||
|
||||
- uses: https://git.incloud.ovh/anuti/Actions/triage-issue@main
|
||||
with:
|
||||
issue-title: ${{ github.event.issue.title }}
|
||||
issue-body: ${{ github.event.issue.body }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
repository: ${{ github.repository }}
|
||||
api-token: ${{ secrets.TOKEN }}
|
||||
api-key: ${{ secrets.OPENCODE_API_KEY }}
|
||||
```
|
||||
|
||||
Se il runner ha una label diversa da `linux_amd64`, modificare `runs-on` di conseguenza.
|
||||
|
||||
### 4. Commit e push
|
||||
|
||||
```bash
|
||||
git add .gitea/workflows/triage-issue.yml
|
||||
git commit -m "Aggiunge workflow triage automatico"
|
||||
git push
|
||||
```
|
||||
|
||||
### 5. Test
|
||||
|
||||
Aprire una nuova issue sul repository. Entro pochi secondi il runner dovrebbe:
|
||||
|
||||
1. Classificare l'issue come `bug` o `richiesta`
|
||||
2. Applicare la label corrispondente
|
||||
3. Pubblicare un commento con riassunto, gap analisi e domande aperte
|
||||
4. Caricare un file `.md` con l'analisi tecnica completa come asset dell'issue
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
**Il workflow non parte**
|
||||
- Verificare che `has_actions: true` sul repository (API: `PATCH /repos/{owner}/{repo}` con body `{"has_actions":true}`)
|
||||
- Verificare che il runner sia online e abbia la label corretta
|
||||
|
||||
**Errore "secret not found"**
|
||||
- Verificare che i secrets `TOKEN` e `OPENCODE_API_KEY` siano configurati
|
||||
|
||||
**Label non trovata**
|
||||
- Il triage cerca label con nome esattamente `bug` e `richiesta`. Crearle se mancanti.
|
||||
136
codex-reply/action.yml
Normal file
136
codex-reply/action.yml
Normal file
@@ -0,0 +1,136 @@
|
||||
name: Codex Reply
|
||||
description: "Risponde a un commento @codex analizzando issue, cronologia e codice repository."
|
||||
|
||||
inputs:
|
||||
issue-number:
|
||||
description: Numero dell'issue.
|
||||
required: true
|
||||
repository:
|
||||
description: Repository in formato owner/repo.
|
||||
required: true
|
||||
comment-body:
|
||||
description: Testo del commento che contiene @codex.
|
||||
required: true
|
||||
comment-author:
|
||||
description: Login dell'autore del commento.
|
||||
required: true
|
||||
issue-title:
|
||||
description: Titolo dell'issue.
|
||||
required: true
|
||||
issue-body:
|
||||
description: Corpo dell'issue.
|
||||
required: true
|
||||
api-token:
|
||||
description: Token API Gitea.
|
||||
required: true
|
||||
gitea-host:
|
||||
description: URL del server Gitea.
|
||||
required: false
|
||||
default: "https://git.incloud.ovh"
|
||||
api-key:
|
||||
description: API key per opencode.
|
||||
required: true
|
||||
model:
|
||||
description: Modello AI in formato provider/model.
|
||||
required: false
|
||||
default: "opencode-go/deepseek-v4-flash"
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Configura autenticazione opencode
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "jq non trovato, tentativo di installazione..."
|
||||
if command -v apt-get &> /dev/null; then
|
||||
apt-get update -qq && apt-get install -y -qq jq 2>&1 || true
|
||||
elif command -v apk &> /dev/null; then
|
||||
apk add --no-cache jq 2>&1 || true
|
||||
fi
|
||||
command -v jq &> /dev/null || { echo "ERRORE: impossibile installare jq."; exit 1; }
|
||||
fi
|
||||
mkdir -p ~/.local/share/opencode
|
||||
jq -n \
|
||||
--arg provider "opencode-go" \
|
||||
--arg key "${{ inputs.api-key }}" \
|
||||
'{($provider): {type: "api", key: $key}}' > ~/.local/share/opencode/auth.json
|
||||
chmod 600 ~/.local/share/opencode/auth.json
|
||||
|
||||
- name: Rispondi a @codex
|
||||
id: codex
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if ! command -v curl &> /dev/null; then
|
||||
echo "curl non trovato, tentativo di installazione..."
|
||||
if command -v apt-get &> /dev/null; then
|
||||
apt-get update -qq && apt-get install -y -qq curl 2>&1 || true
|
||||
elif command -v apk &> /dev/null; then
|
||||
apk add --no-cache curl 2>&1 || true
|
||||
fi
|
||||
command -v curl &> /dev/null || { echo "ERRORE: impossibile installare curl."; exit 1; }
|
||||
fi
|
||||
|
||||
HOST="${{ inputs.gitea-host }}"
|
||||
TOKEN="${{ inputs.api-token }}"
|
||||
REPO="${{ inputs.repository }}"
|
||||
ISSUE_NUM="${{ inputs.issue-number }}"
|
||||
AUTHOR="${{ inputs.comment-author }}"
|
||||
MODEL_ARG=()
|
||||
[ -n "${{ inputs.model }}" ] && MODEL_ARG=(--model "${{ inputs.model }}")
|
||||
|
||||
# Estrai la domanda dopo @codex
|
||||
QUERY=$(echo "${{ inputs.comment-body }}" | sed -n 's/.*@codex[[:space:],:]*//p')
|
||||
if [ -z "$QUERY" ]; then
|
||||
QUERY="(nessuna domanda specifica dopo @codex)"
|
||||
fi
|
||||
|
||||
# Recupera cronologia commenti, escludendo quelli del bot
|
||||
COMMENTS_JSON=$(curl -sS -f "$HOST/api/v1/repos/$REPO/issues/$ISSUE_NUM/comments" \
|
||||
-H "Authorization: token $TOKEN" \
|
||||
-H "Accept: application/json" 2>/dev/null || echo "[]")
|
||||
COMMENTS_TEXT=$(echo "$COMMENTS_JSON" | jq -r '
|
||||
[.[] | select(.user.login != "codex")]
|
||||
| sort_by(.created_at)
|
||||
| .[-20:]
|
||||
| .[]
|
||||
| "**\(.user.login)**: \(.body)\n"
|
||||
' 2>/dev/null || echo "")
|
||||
|
||||
# Costruisci il prompt
|
||||
BODY=$(echo "${{ inputs.issue-body }}" | head -c 10000)
|
||||
[ "${#BODY}" -ge 10000 ] && BODY+=$'\n... (troncato)'
|
||||
|
||||
printf -v PROMPT '%s\n\n%s\n%s\n\n## %s\n%s\n%s\n\n## %s\n%s\n\n## %s - @%s\n%s' \
|
||||
"Sei un assistente esperto per repository software." \
|
||||
"Rispondi alla domanda in modo diretto e tecnico, in italiano." \
|
||||
"Analizza il codice sorgente nel repository quando e' rilevante." \
|
||||
"Issue" \
|
||||
"**Titolo**: ${{ inputs.issue-title }}" \
|
||||
"**Corpo**: $BODY" \
|
||||
"Cronologia commenti" "$COMMENTS_TEXT" \
|
||||
"Domanda" "$AUTHOR" "$QUERY"
|
||||
|
||||
# Esegui opencode
|
||||
OUTFILE="$GITHUB_WORKSPACE/opencode-codex.txt"
|
||||
opencode run "$PROMPT" "${MODEL_ARG[@]}" --dangerously-skip-permissions 2>&1 | tee "$OUTFILE"
|
||||
|
||||
# Pulisci la risposta: togli ANSI codes, banner build, righe vuote iniziali
|
||||
ESC=$(printf '\x1b')
|
||||
sed "s/${ESC}\[[0-9;]*m//g" "$OUTFILE" > /tmp/codex-clean.txt
|
||||
RESPONSE=$(sed '/^> build /d; /^[[:space:]]*$/d; s/\\n/\n/g' /tmp/codex-clean.txt)
|
||||
if [ -z "$RESPONSE" ]; then
|
||||
RESPONSE="Mi dispiace, non ho generato una risposta. Riprova."
|
||||
fi
|
||||
|
||||
# Pubblica il commento
|
||||
COMMENT_JSON=$(jq -n --arg body "$RESPONSE" '{body: $body}')
|
||||
curl -sS -X POST "$HOST/api/v1/repos/$REPO/issues/$ISSUE_NUM/comments" \
|
||||
-H "Authorization: token $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$COMMENT_JSON" 2>/dev/null && \
|
||||
echo "Risposta @codex pubblicata sull'issue #$ISSUE_NUM" || \
|
||||
echo "WARN: Impossibile pubblicare la risposta"
|
||||
144
deploy-iis/action.yml
Normal file
144
deploy-iis/action.yml
Normal file
@@ -0,0 +1,144 @@
|
||||
name: Deploy IIS
|
||||
description: Ferma sito e application pool IIS, sincronizza i file pubblicati e riavvia i servizi.
|
||||
|
||||
inputs:
|
||||
source-path:
|
||||
description: Cartella sorgente da distribuire su IIS.
|
||||
required: true
|
||||
destination-path:
|
||||
description: Cartella di destinazione sul server IIS.
|
||||
required: true
|
||||
site-name:
|
||||
description: Nome del sito IIS da fermare e riavviare.
|
||||
required: true
|
||||
app-pool-name:
|
||||
description: Nome dell'application pool IIS da fermare e riavviare.
|
||||
required: true
|
||||
exclude-dirs:
|
||||
description: Elenco di directory da escludere dal mirroring (separate da virgola, punto e virgola o newline).
|
||||
required: false
|
||||
default: store
|
||||
exclude-files:
|
||||
description: Elenco di file da escludere dal mirroring (separati da virgola, punto e virgola o newline).
|
||||
required: false
|
||||
default: appsettings.json
|
||||
clean-destination:
|
||||
description: Se 'true', elimina tutto il contenuto della cartella di destinazione (dopo aver fermato IIS) tranne appsettings.json, web.config e la cartella store.
|
||||
required: false
|
||||
default: 'false'
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Deploy su IIS
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$appcmd = Join-Path $env:SystemRoot 'System32\inetsrv\appcmd.exe'
|
||||
$src = '${{ inputs.source-path }}'
|
||||
$dst = '${{ inputs.destination-path }}'
|
||||
$site = '${{ inputs.site-name }}'
|
||||
$appPool = '${{ inputs.app-pool-name }}'
|
||||
|
||||
function Split-InputList {
|
||||
param([string]$Value)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Value)) {
|
||||
return @()
|
||||
}
|
||||
|
||||
return @(
|
||||
$Value -split '[,;\r\n]+'
|
||||
| ForEach-Object { $_.Trim() }
|
||||
| Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
|
||||
)
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $appcmd)) {
|
||||
throw "appcmd.exe non trovato: $appcmd"
|
||||
}
|
||||
|
||||
foreach ($entry in @{
|
||||
'source-path' = $src
|
||||
'destination-path' = $dst
|
||||
'site-name' = $site
|
||||
'app-pool-name' = $appPool
|
||||
}.GetEnumerator()) {
|
||||
if ([string]::IsNullOrWhiteSpace($entry.Value)) {
|
||||
throw "Input '$($entry.Key)' mancante."
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $src)) {
|
||||
throw "Cartella sorgente non trovata: $src"
|
||||
}
|
||||
|
||||
$excludeDirs = Split-InputList '${{ inputs.exclude-dirs }}'
|
||||
$excludeFiles = Split-InputList '${{ inputs.exclude-files }}'
|
||||
|
||||
$robocopyArgs = @(
|
||||
$src
|
||||
$dst
|
||||
'/MIR'
|
||||
'/R:2'
|
||||
'/W:1'
|
||||
'/NFL'
|
||||
'/NDL'
|
||||
'/NP'
|
||||
)
|
||||
|
||||
if ($excludeDirs.Count -gt 0) {
|
||||
$robocopyArgs += '/XD'
|
||||
$robocopyArgs += $excludeDirs
|
||||
}
|
||||
|
||||
if ($excludeFiles.Count -gt 0) {
|
||||
$robocopyArgs += '/XF'
|
||||
$robocopyArgs += $excludeFiles
|
||||
}
|
||||
|
||||
$siteStopped = $false
|
||||
$poolStopped = $false
|
||||
|
||||
try {
|
||||
& $appcmd stop site "/site.name:$site"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Warning "stop site fallito ($LASTEXITCODE) - si prosegue comunque"
|
||||
} else {
|
||||
$siteStopped = $true
|
||||
}
|
||||
|
||||
& $appcmd stop apppool "/apppool.name:$appPool"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Warning "stop apppool fallito ($LASTEXITCODE) - si prosegue comunque"
|
||||
} else {
|
||||
$poolStopped = $true
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $dst | Out-Null
|
||||
|
||||
if ('${{ inputs.clean-destination }}' -eq 'true') {
|
||||
$keepItems = @('appsettings.json', 'web.config', 'store')
|
||||
Get-ChildItem -Path $dst | Where-Object { $_.Name -notin $keepItems } | Remove-Item -Recurse -Force
|
||||
Write-Host "Pulizia destinazione completata (keep: $($keepItems -join ', '))"
|
||||
}
|
||||
|
||||
& robocopy @robocopyArgs
|
||||
$robocopyExitCode = $LASTEXITCODE
|
||||
if ($robocopyExitCode -ge 8) {
|
||||
throw "robocopy fallito ($robocopyExitCode)"
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if ($poolStopped) {
|
||||
& $appcmd start apppool "/apppool.name:$appPool"
|
||||
if ($LASTEXITCODE -ne 0) { throw "start apppool fallito ($LASTEXITCODE)" }
|
||||
}
|
||||
|
||||
if ($siteStopped) {
|
||||
& $appcmd start site "/site.name:$site"
|
||||
if ($LASTEXITCODE -ne 0) { throw "start site fallito ($LASTEXITCODE)" }
|
||||
}
|
||||
}
|
||||
|
||||
32
example-workflow.yml
Normal file
32
example-workflow.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
name: Build and Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: [v*]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Estrai versione dal tag
|
||||
id: ver
|
||||
uses: https://<host>/<owner>/Actions/version-from-tag@v1.2.3
|
||||
with:
|
||||
ref-name: ${{ github.ref_name }}
|
||||
|
||||
- name: Publish .NET
|
||||
uses: https://<host>/<owner>/Actions/publish-dotnet@v1.2.3
|
||||
with:
|
||||
project: src/MyApp/MyApp.csproj
|
||||
output-path: publish
|
||||
version: ${{ steps.ver.outputs.appver }}
|
||||
|
||||
- name: Deploy IIS
|
||||
uses: https://<host>/<owner>/Actions/deploy-iis@v1.2.3
|
||||
with:
|
||||
source-path: publish
|
||||
destination-path: C:\inetpub\wwwroot\myapp
|
||||
site-name: MySite
|
||||
app-pool-name: MyAppPool
|
||||
49
install-opencode/action.yml
Normal file
49
install-opencode/action.yml
Normal file
@@ -0,0 +1,49 @@
|
||||
name: Installa OpenCode
|
||||
description: Installa opencode-ai globalmente tramite npm.
|
||||
|
||||
inputs:
|
||||
version:
|
||||
description: "Versione di opencode da installare (default: latest)."
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Verifica prerequisiti e installa npm se assente
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! command -v node &> /dev/null; then
|
||||
echo "ERRORE: node non trovato. Installa Node.js sul runner."
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v npm &> /dev/null; then
|
||||
echo "npm non trovato, tentativo di installazione..."
|
||||
if command -v apt-get &> /dev/null; then
|
||||
apt-get update -qq && apt-get install -y -qq npm 2>&1 || true
|
||||
elif command -v apk &> /dev/null; then
|
||||
apk add --no-cache npm 2>&1 || true
|
||||
fi
|
||||
if ! command -v npm &> /dev/null; then
|
||||
echo "ERRORE: impossibile installare npm."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo "node $(node --version), npm $(npm --version)"
|
||||
|
||||
- name: Installa opencode
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if command -v opencode &> /dev/null; then
|
||||
echo "opencode già installato: $(opencode --version)"
|
||||
exit 0
|
||||
fi
|
||||
VERSION="${{ inputs.version }}"
|
||||
if [ -n "$VERSION" ]; then
|
||||
npm install -g "opencode-ai@${VERSION}"
|
||||
else
|
||||
npm install -g opencode-ai
|
||||
fi
|
||||
echo "opencode installato: $(opencode --version)"
|
||||
64
opencode-prompt/action.yml
Normal file
64
opencode-prompt/action.yml
Normal file
@@ -0,0 +1,64 @@
|
||||
name: OpenCode Prompt
|
||||
description: Esegue un prompt opencode sul codice del repository.
|
||||
|
||||
inputs:
|
||||
prompt:
|
||||
description: Il prompt da passare a opencode.
|
||||
required: true
|
||||
api-key:
|
||||
description: API key per il provider opencode.
|
||||
required: true
|
||||
api-provider:
|
||||
description: "Nome del provider (default: opencode-go)."
|
||||
required: false
|
||||
default: "opencode-go"
|
||||
model:
|
||||
description: Modello AI (formato provider/model).
|
||||
required: false
|
||||
default: "opencode-go/deepseek-v4-flash"
|
||||
agent:
|
||||
description: Agente opencode da utilizzare.
|
||||
required: false
|
||||
default: ""
|
||||
working-directory:
|
||||
description: Directory di lavoro per opencode. Se vuoto, usa la radice del repo.
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
outputs:
|
||||
result:
|
||||
description: Output prodotto da opencode.
|
||||
value: ${{ steps.esegui.outputs.result }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Configura autenticazione opencode
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p ~/.local/share/opencode
|
||||
jq -n \
|
||||
--arg provider "${{ inputs.api-provider }}" \
|
||||
--arg key "${{ inputs.api-key }}" \
|
||||
'{($provider): {type: "api", key: $key}}' > ~/.local/share/opencode/auth.json
|
||||
chmod 600 ~/.local/share/opencode/auth.json
|
||||
|
||||
- name: Esegui opencode
|
||||
id: esegui
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
WD="${{ inputs.working-directory }}"
|
||||
[ -z "$WD" ] && WD="$GITHUB_WORKSPACE"
|
||||
cd "$WD"
|
||||
EXTRA_ARGS=()
|
||||
[ -n "${{ inputs.model }}" ] && EXTRA_ARGS+=(--model "${{ inputs.model }}")
|
||||
[ -n "${{ inputs.agent }}" ] && EXTRA_ARGS+=(--agent "${{ inputs.agent }}")
|
||||
opencode run "${{ inputs.prompt }}" "${EXTRA_ARGS[@]}" --dangerously-skip-permissions 2>&1 | tee "$GITHUB_WORKSPACE/opencode-output.txt"
|
||||
RESULT=$(cat "$GITHUB_WORKSPACE/opencode-output.txt")
|
||||
{
|
||||
echo "result<<EOF"
|
||||
echo "$RESULT"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
79
publish-dotnet/action.yml
Normal file
79
publish-dotnet/action.yml
Normal file
@@ -0,0 +1,79 @@
|
||||
name: Publish .NET
|
||||
description: >
|
||||
Esegue restore, publish e rsync per un progetto .NET.
|
||||
|
||||
inputs:
|
||||
project:
|
||||
description: Path al file .csproj del progetto.
|
||||
required: true
|
||||
output-path:
|
||||
description: Directory di destinazione per i file pubblicati.
|
||||
required: true
|
||||
version:
|
||||
description: Versione applicativa da applicare (es. 1.0.0.0).
|
||||
required: true
|
||||
subpath:
|
||||
description: >
|
||||
Sottopath relativo alla cartella di publish da copiare
|
||||
(es. "wwwroot" per Blazor WASM). Vuoto per copiare tutto.
|
||||
required: false
|
||||
default: ""
|
||||
configuration:
|
||||
description: Configurazione di build (es. Release, Debug).
|
||||
required: false
|
||||
default: Release
|
||||
exclude-dirs:
|
||||
description: Directory da escludere dal rsync (separate da virgola, punto e virgola o newline).
|
||||
required: false
|
||||
default: store
|
||||
exclude-files:
|
||||
description: File da escludere dal rsync (separati da virgola, punto e virgola o newline).
|
||||
required: false
|
||||
default: appsettings.json
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Restore
|
||||
shell: bash
|
||||
run: dotnet restore "${{ inputs.project }}"
|
||||
|
||||
- name: Publish e rsync
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
split_input_list() {
|
||||
local value="$1"
|
||||
if [ -z "$value" ]; then
|
||||
echo ""
|
||||
return
|
||||
fi
|
||||
echo "$value" | tr ',;\r\n' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -v '^$'
|
||||
}
|
||||
|
||||
tmpdir=$(mktemp -d)
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
|
||||
dotnet publish "${{ inputs.project }}" \
|
||||
-c "${{ inputs.configuration }}" \
|
||||
-p:Version="${{ inputs.version }}" \
|
||||
-o "$tmpdir"
|
||||
|
||||
src="$tmpdir"
|
||||
if [ -n "${{ inputs.subpath }}" ]; then
|
||||
src="$tmpdir/${{ inputs.subpath }}"
|
||||
fi
|
||||
|
||||
mkdir -p "${{ inputs.output-path }}"
|
||||
|
||||
exclude_args=()
|
||||
while IFS= read -r dir; do
|
||||
[ -n "$dir" ] && exclude_args+=(--exclude="$dir")
|
||||
done < <(split_input_list "${{ inputs.exclude-dirs }}")
|
||||
|
||||
while IFS= read -r file; do
|
||||
[ -n "$file" ] && exclude_args+=(--exclude="$file")
|
||||
done < <(split_input_list "${{ inputs.exclude-files }}")
|
||||
|
||||
rsync -a --delete "${exclude_args[@]}" "$src/" "${{ inputs.output-path }}/"
|
||||
182
triage-issue/action.yml
Normal file
182
triage-issue/action.yml
Normal file
@@ -0,0 +1,182 @@
|
||||
name: Triage Issue
|
||||
description: "Analizza una issue con opencode: classifica, riassume, gap analisi e produce report MD."
|
||||
|
||||
inputs:
|
||||
issue-title:
|
||||
description: Titolo dell'issue.
|
||||
required: true
|
||||
issue-body:
|
||||
description: Corpo dell'issue.
|
||||
required: true
|
||||
issue-number:
|
||||
description: Numero dell'issue.
|
||||
required: true
|
||||
repository:
|
||||
description: Repository in formato owner/repo.
|
||||
required: true
|
||||
api-token:
|
||||
description: Token API Gitea.
|
||||
required: true
|
||||
gitea-host:
|
||||
description: URL del server Gitea.
|
||||
required: false
|
||||
default: "https://git.incloud.ovh"
|
||||
api-key:
|
||||
description: API key per opencode.
|
||||
required: true
|
||||
model:
|
||||
description: Modello AI in formato provider/model.
|
||||
required: false
|
||||
default: "opencode-go/deepseek-v4-flash"
|
||||
|
||||
outputs:
|
||||
label:
|
||||
description: Label attribuita (bug o richiesta).
|
||||
value: ${{ steps.classifica.outputs.label }}
|
||||
comment:
|
||||
description: Commento di triage pubblicato.
|
||||
value: ${{ steps.classifica.outputs.comment }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Configura autenticazione opencode
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "jq non trovato, tentativo di installazione..."
|
||||
if command -v apt-get &> /dev/null; then
|
||||
apt-get update -qq && apt-get install -y -qq jq 2>&1 || true
|
||||
elif command -v apk &> /dev/null; then
|
||||
apk add --no-cache jq 2>&1 || true
|
||||
fi
|
||||
command -v jq &> /dev/null || { echo "ERRORE: impossibile installare jq."; exit 1; }
|
||||
fi
|
||||
mkdir -p ~/.local/share/opencode
|
||||
jq -n \
|
||||
--arg provider "opencode-go" \
|
||||
--arg key "${{ inputs.api-key }}" \
|
||||
'{($provider): {type: "api", key: $key}}' > ~/.local/share/opencode/auth.json
|
||||
chmod 600 ~/.local/share/opencode/auth.json
|
||||
|
||||
- name: Classifica issue
|
||||
id: classifica
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Assicura curl (potrebbe mancare su Alpine)
|
||||
if ! command -v curl &> /dev/null; then
|
||||
echo "curl non trovato, tentativo di installazione..."
|
||||
if command -v apt-get &> /dev/null; then
|
||||
apt-get update -qq && apt-get install -y -qq curl 2>&1 || true
|
||||
elif command -v apk &> /dev/null; then
|
||||
apk add --no-cache curl 2>&1 || true
|
||||
fi
|
||||
command -v curl &> /dev/null || { echo "ERRORE: impossibile installare curl."; exit 1; }
|
||||
fi
|
||||
|
||||
HOST="${{ inputs.gitea-host }}"
|
||||
TOKEN="${{ inputs.api-token }}"
|
||||
REPO="${{ inputs.repository }}"
|
||||
ISSUE_NUM="${{ inputs.issue-number }}"
|
||||
MODEL_ARG=()
|
||||
[ -n "${{ inputs.model }}" ] && MODEL_ARG=(--model "${{ inputs.model }}")
|
||||
|
||||
BODY="$(echo "${{ inputs.issue-body }}" | head -c 10000)"
|
||||
[ "${#BODY}" -ge 10000 ] && BODY+=$'\n... (troncato)'
|
||||
|
||||
printf -v PROMPT '%s\n\n%s\n\n%s\n\n%s\n\n%s\n\n%s\n\n%s\n\n%s\n\n%s: %s\n%s:\n%s' \
|
||||
"Sei un sistema di triage automatico per repository software." \
|
||||
"Analizza il codice sorgente nel repository ed esamina la seguente issue." \
|
||||
"Classifica con UNA delle label:" \
|
||||
'- "bug": malfunzionamento, errore, crash, anomalia, comportamento inaspettato' \
|
||||
'- "richiesta": nuova funzionalità, miglioramento, refactoring, ottimizzazione, o dubbio' \
|
||||
"Rispondi ESCLUSIVAMENTE con un JSON valido su una SINGOLA riga, senza nessun altro testo:" \
|
||||
'{"label":"bug","comment":"**Riassunto**: ...\n\n**Gap analisi**: codice mancante...\n\n**Domande aperte**:\n1. ...","md":"# Analisi tecnica\n\n## Codice coinvolto\n..."}' \
|
||||
"Il campo 'comment' deve contenere: riassunto della issue, gap analisi (cosa manca rispetto al codice), domande aperte. Usa \\n per i newline." \
|
||||
"Il campo 'md' deve contenere: analisi tecnica completa in formato markdown, file/moduli coinvolti, ipotesi root cause, proposta di fix. Usa \\n per i newline." \
|
||||
"Titolo" "${{ inputs.issue-title }}" \
|
||||
"Corpo" "$BODY"
|
||||
|
||||
OUTFILE="$GITHUB_WORKSPACE/opencode-triage.txt"
|
||||
opencode run "$PROMPT" "${MODEL_ARG[@]}" --dangerously-skip-permissions 2>&1 | tee "$OUTFILE"
|
||||
|
||||
# Estrai la riga JSON dalla risposta (salta banner ANSI)
|
||||
JSON_LINE=$(grep -E '^\{"label":"(bug|richiesta)","comment":' "$OUTFILE" | head -1)
|
||||
JSON=""
|
||||
if [ -n "$JSON_LINE" ]; then
|
||||
JSON=$(echo "$JSON_LINE" | jq -c . 2>/dev/null)
|
||||
fi
|
||||
|
||||
if [ -z "$JSON" ] || ! echo "$JSON" | jq empty 2>/dev/null; then
|
||||
echo "WARN: Impossibile estrarre JSON valido, default a richiesta"
|
||||
LABEL="richiesta"
|
||||
COMMENT="Classificazione automatica non riuscita. Label impostata a richiesta per default."
|
||||
MD_CONTENT=""
|
||||
else
|
||||
LABEL=$(echo "$JSON" | jq -r '.label // "richiesta"')
|
||||
COMMENT=$(echo "$JSON" | jq -r '.comment // "Classificazione automatica."')
|
||||
COMMENT=$(printf '%b' "$COMMENT")
|
||||
MD_CONTENT=$(echo "$JSON" | jq -r '.md // ""')
|
||||
MD_CONTENT=$(printf '%b' "$MD_CONTENT")
|
||||
fi
|
||||
|
||||
case "$LABEL" in
|
||||
bug|richiesta) ;;
|
||||
*) echo "WARN: Label '$LABEL' sconosciuta, default a richiesta"; LABEL="richiesta" ;;
|
||||
esac
|
||||
|
||||
echo "label=$LABEL" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo "comment<<EOF"
|
||||
echo "$COMMENT"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "Label determinata: $LABEL"
|
||||
|
||||
LABEL_ID=$(curl -sS -f "$HOST/api/v1/repos/$REPO/labels" \
|
||||
-H "Authorization: token $TOKEN" \
|
||||
-H "Accept: application/json" \
|
||||
2>/dev/null | jq -r ".[] | select(.name==\"$LABEL\") | .id" | head -1)
|
||||
|
||||
if [ -n "$LABEL_ID" ]; then
|
||||
curl -sS -X PUT "$HOST/api/v1/repos/$REPO/issues/$ISSUE_NUM/labels" \
|
||||
-H "Authorization: token $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"labels\":[$LABEL_ID]}" 2>/dev/null && \
|
||||
echo "Label '$LABEL' applicata" || \
|
||||
echo "WARN: Impossibile applicare la label '$LABEL'"
|
||||
else
|
||||
echo "WARN: Label '$LABEL' non trovata nel repo. Creala manualmente o passa un token con permessi."
|
||||
fi
|
||||
|
||||
# Carica il file MD come asset (prima del commento per poterlo linkare)
|
||||
LINK_MD=""
|
||||
if [ -n "$MD_CONTENT" ] && [ "$MD_CONTENT" != "null" ]; then
|
||||
MD_FILE="/tmp/triage-issue-${ISSUE_NUM}.md"
|
||||
printf '%s\n' "$MD_CONTENT" > "$MD_FILE"
|
||||
|
||||
ASSET_RESP=$(curl -sS -X POST "$HOST/api/v1/repos/$REPO/issues/$ISSUE_NUM/assets" \
|
||||
-H "Authorization: token $TOKEN" \
|
||||
-H "Accept: application/json" \
|
||||
-F "attachment=@${MD_FILE};filename=triage-issue-${ISSUE_NUM}.md" 2>/dev/null)
|
||||
ASSET_URL=$(echo "$ASSET_RESP" | jq -r '.browser_download_url // empty' 2>/dev/null)
|
||||
if [ -n "$ASSET_URL" ] && [ "$ASSET_URL" != "null" ]; then
|
||||
printf -v LINK_MD '\n\n---\n📎 **Analisi tecnica completa**: [triage-issue-%s.md](%s)' "$ISSUE_NUM" "$ASSET_URL"
|
||||
echo "Asset MD caricato: $ASSET_URL"
|
||||
else
|
||||
echo "WARN: Impossibile caricare il file MD come asset"
|
||||
fi
|
||||
fi
|
||||
|
||||
COMMENT_BODY="${COMMENT}${LINK_MD}"
|
||||
COMMENT_JSON=$(jq -n --arg body "$COMMENT_BODY" '{body: $body}')
|
||||
curl -sS -X POST "$HOST/api/v1/repos/$REPO/issues/$ISSUE_NUM/comments" \
|
||||
-H "Authorization: token $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$COMMENT_JSON" 2>/dev/null && \
|
||||
echo "Commento pubblicato sull'issue #$ISSUE_NUM" || \
|
||||
echo "WARN: Impossibile pubblicare il commento"
|
||||
55
version-from-tag/action.yml
Normal file
55
version-from-tag/action.yml
Normal file
@@ -0,0 +1,55 @@
|
||||
name: Versione da tag
|
||||
description: Valida il tag in compilazione e popola le variabili di versionamento.
|
||||
|
||||
inputs:
|
||||
ref-name:
|
||||
description: Nome del tag o ref da elaborare.
|
||||
required: true
|
||||
|
||||
outputs:
|
||||
appver:
|
||||
description: Versione applicativa in formato n.n.n.n.
|
||||
value: ${{ steps.versione.outputs.appver }}
|
||||
fullver:
|
||||
description: Versione completa, comprensiva di eventuale suffisso.
|
||||
value: ${{ steps.versione.outputs.fullver }}
|
||||
suffix:
|
||||
description: Suffisso estratto dal tag, comprensivo del trattino iniziale quando presente.
|
||||
value: ${{ steps.versione.outputs.suffix }}
|
||||
version:
|
||||
description: Versione normalizzata senza punti, mantenendo l'eventuale suffisso con trattino.
|
||||
value: ${{ steps.versione.outputs.version }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Calcola versione da tag
|
||||
id: versione
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
ref_name='${{ inputs.ref-name }}'
|
||||
|
||||
if [[ -z "$ref_name" ]]; then
|
||||
echo "Input 'ref-name' mancante."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tag="${ref_name#v}"
|
||||
|
||||
if [[ "$tag" =~ ^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)(-.+)?$ ]]; then
|
||||
appver="${BASH_REMATCH[1]}"
|
||||
suffix="${BASH_REMATCH[2]:-}"
|
||||
else
|
||||
echo "Formato tag non valido: '$ref_name'. Atteso: v1.0.0.0 oppure v1.0.0.0-suffisso"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
fullver="$appver$suffix"
|
||||
version="${appver//./}$suffix"
|
||||
|
||||
echo "appver=$appver" >> "$GITHUB_OUTPUT"
|
||||
echo "fullver=$fullver" >> "$GITHUB_OUTPUT"
|
||||
echo "suffix=$suffix" >> "$GITHUB_OUTPUT"
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
Reference in New Issue
Block a user