Build release packages in GitHub Actions

This commit is contained in:
ushan0v
2026-05-14 15:44:12 +03:00
parent 40c28e34fe
commit 13b32d3701
7 changed files with 192 additions and 842 deletions
+79 -23
View File
@@ -3,42 +3,87 @@ on:
push:
tags:
- '*'
workflow_dispatch:
inputs:
release_tag:
description: Release tag to build and publish
required: true
type: string
env:
RELEASE_TAG: ${{ github.event.inputs.release_tag || github.ref_name }}
permissions:
contents: write
jobs:
build:
name: Build Podkop Plus packages
preparation:
name: Setup build version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v5.0.0
with:
fetch-depth: 0
- id: version
run: |
VERSION=$(git describe --tags --exact-match 2>/dev/null || echo "0.$(date +%d%m%Y)")
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
build:
name: Builder for ${{ matrix.package_type }} Podkop Plus packages
runs-on: ubuntu-latest
needs: preparation
strategy:
matrix:
include:
- { package_type: ipk }
- { package_type: apk }
steps:
- uses: actions/checkout@v5.0.0
with:
fetch-depth: 0
- uses: actions/setup-node@v4
- name: Build ${{ matrix.package_type }}
uses: docker/build-push-action@v6.18.0
with:
node-version: '22'
file: ./Dockerfile-${{ matrix.package_type }}
context: .
tags: podkop-plus:ci-${{ matrix.package_type }}
build-args: |
PODKOP_VERSION=${{ needs.preparation.outputs.version }}
- name: Build release artifacts
- name: Create ${{ matrix.package_type }} Docker container
run: docker create --name ${{ matrix.package_type }} podkop-plus:ci-${{ matrix.package_type }}
- name: Copy files from ${{ matrix.package_type }} Docker container
run: |
mkdir -p ./bin/${{ matrix.package_type }}
docker cp ${{ matrix.package_type }}:/builder/bin/packages/x86_64/utilities/. ./bin/${{ matrix.package_type }}/
docker cp ${{ matrix.package_type }}:/builder/bin/packages/x86_64/luci/. ./bin/${{ matrix.package_type }}/
# IPK uses underscore `_` in filenames, while APK uses only dash `-`.
- name: Fix naming difference between build for packages
if: matrix.package_type == 'ipk'
shell: bash
run: |
bash ./scripts/build-openwrt-packages-wsl.sh "${RELEASE_TAG}" ./dist/release
for f in ./bin/${{ matrix.package_type }}/*.${{ matrix.package_type }}; do
[ -e "$f" ] || continue
base=$(basename "$f")
newname=$(echo "$base" | sed 's/_/-/g')
mv "$f" "./bin/${{ matrix.package_type }}/$newname"
done
- name: Filter files
shell: bash
run: |
VERSION="${{ needs.preparation.outputs.version }}"
mkdir -p ./filtered-bin/${{ matrix.package_type }}
cp ./bin/${{ matrix.package_type }}/luci-i18n-podkop-plus-ru-*.${{ matrix.package_type }} "./filtered-bin/${{ matrix.package_type }}/luci-i18n-podkop-plus-ru-${VERSION}.${{ matrix.package_type }}"
cp ./bin/${{ matrix.package_type }}/podkop-plus-*.${{ matrix.package_type }} ./filtered-bin/${{ matrix.package_type }}/
cp ./bin/${{ matrix.package_type }}/luci-app-podkop-plus-*.${{ matrix.package_type }} ./filtered-bin/${{ matrix.package_type }}/
- name: Remove Docker container
run: docker rm ${{ matrix.package_type }}
- name: Upload build artifacts
uses: actions/upload-artifact@v4.6.2
with:
name: release-files-${{ env.RELEASE_TAG }}
path: ./dist/release/*.*
name: release-files-${{ github.ref_name }}-${{ matrix.package_type }}
path: ./filtered-bin/${{ matrix.package_type }}/*.${{ matrix.package_type }}
retention-days: 1
if-no-files-found: error
@@ -47,16 +92,27 @@ jobs:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/download-artifact@v4
- uses: actions/checkout@v5.0.0
- name: Create release dir
run: mkdir -p ./filtered-bin/release
- name: Download ipk artifacts
uses: actions/download-artifact@v4
with:
name: release-files-${{ env.RELEASE_TAG }}
path: ./dist/release
name: release-files-${{ github.ref_name }}-ipk
path: ./filtered-bin/release
- name: Download apk artifacts
uses: actions/download-artifact@v4
with:
name: release-files-${{ github.ref_name }}-apk
path: ./filtered-bin/release
- name: Release
uses: softprops/action-gh-release@v2.4.0
with:
files: ./dist/release/*.*
files: ./filtered-bin/release/*.*
draft: false
prerelease: false
name: ${{ env.RELEASE_TAG }}
tag_name: ${{ env.RELEASE_TAG }}
name: ${{ github.ref_name }}
tag_name: ${{ github.ref_name }}
+1 -3
View File
@@ -6,8 +6,6 @@ ENV PODKOP_VERSION=${PODKOP_VERSION}
COPY ./podkop /builder/package/feeds/utilities/podkop-plus
COPY ./luci-app-podkop-plus /builder/package/feeds/luci/luci-app-podkop-plus
RUN ./scripts/feeds update -a && \
./scripts/feeds install -a && \
make defconfig && \
RUN make defconfig && \
make package/podkop-plus/compile -j4 V=s && \
make package/luci-app-podkop-plus/compile -j4 V=s
-2
View File
@@ -6,8 +6,6 @@ COPY ./podkop /builder/package/feeds/utilities/podkop-plus
COPY ./luci-app-podkop-plus /builder/package/feeds/luci/luci-app-podkop-plus
RUN export PODKOP_VERSION="${PODKOP_VERSION}" && \
./scripts/feeds update -a && \
./scripts/feeds install -a && \
make defconfig && \
make package/podkop-plus/compile V=s -j4 && \
make package/luci-app-podkop-plus/compile V=s -j4
@@ -0,0 +1,27 @@
const COPYABLE_PROXY_URI_RE =
/^(vless|vmess|trojan|ss|ssr|hysteria2|hy2|tuic|socks4|socks4a|socks5):\/\//i;
const COPYABLE_PROXY_OUTBOUND_TYPES = new Set([
'vless',
'vmess',
'trojan',
'shadowsocks',
'ss',
'shadowsocksr',
'ssr',
'hysteria2',
'hy2',
'tuic',
'socks',
'socks4',
'socks4a',
'socks5',
]);
export function isCopyableProxyLink(link?: string) {
return COPYABLE_PROXY_URI_RE.test((link || '').trim());
}
export function isCopyableProxyOutboundType(type?: string) {
return COPYABLE_PROXY_OUTBOUND_TYPES.has((type || '').trim().toLowerCase());
}
@@ -0,0 +1,58 @@
import { describe, expect, it } from 'vitest';
import {
isCopyableProxyLink,
isCopyableProxyOutboundType,
} from '../isCopyableProxyLink';
const copyableLinks = [
'vless://uuid@example.com:443',
'vmess://encoded',
'trojan://password@example.com:443',
'ss://encoded@example.com:443',
'ssr://encoded',
'hysteria2://password@example.com:443',
'hy2://password@example.com:443',
'tuic://uuid:password@example.com:443',
'socks4://example.com:1080',
'socks4a://example.com:1080',
'socks5://user:pass@example.com:1080',
];
const nonCopyableLinks = [
'',
'direct',
'block',
'urltest',
'https://example.com/subscription',
'wireguard://example.com',
];
describe('isCopyableProxyLink', () => {
describe.each(copyableLinks)('copyable proxy URI %s', (link) => {
it('returns true', () => {
expect(isCopyableProxyLink(link)).toBe(true);
});
});
describe.each(nonCopyableLinks)('non-copyable value %s', (link) => {
it('returns false', () => {
expect(isCopyableProxyLink(link)).toBe(false);
});
});
});
describe('isCopyableProxyOutboundType', () => {
it('accepts protocol outbound types from imported proxy links', () => {
expect(isCopyableProxyOutboundType('VLESS')).toBe(true);
expect(isCopyableProxyOutboundType('shadowsocks')).toBe(true);
expect(isCopyableProxyOutboundType('hysteria2')).toBe(true);
expect(isCopyableProxyOutboundType('socks')).toBe(true);
});
it('rejects group, service and routing-only outbound types', () => {
expect(isCopyableProxyOutboundType('urltest')).toBe(false);
expect(isCopyableProxyOutboundType('selector')).toBe(false);
expect(isCopyableProxyOutboundType('direct')).toBe(false);
expect(isCopyableProxyOutboundType('block')).toBe(false);
});
});
@@ -0,0 +1,27 @@
import { svgEl } from '../helpers/svgEl';
export function renderLinkIcon24() {
return svgEl(
'svg',
{
xmlns: 'http://www.w3.org/2000/svg',
width: '24',
height: '24',
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
class: 'lucide lucide-link-icon lucide-link',
},
[
svgEl('path', {
d: 'M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71',
}),
svgEl('path', {
d: 'M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71',
}),
],
);
}
-814
View File
@@ -1,814 +0,0 @@
[CmdletBinding()]
param(
[string]$Router = '192.168.1.1',
[string]$User = 'root',
[int]$Port = 22
)
Set-StrictMode -Version 3.0
$ErrorActionPreference = 'Stop'
function Get-RepoRoot {
param([string]$StartPath)
$currentPath = [System.IO.Path]::GetFullPath($StartPath)
while ($true) {
$hasPodkop = Test-Path -LiteralPath (Join-Path $currentPath 'podkop')
$hasLuciApp = Test-Path -LiteralPath (Join-Path $currentPath 'luci-app-podkop-plus')
if ($hasPodkop -and $hasLuciApp) {
return $currentPath
}
$parentPath = Split-Path -Parent $currentPath
if ([string]::IsNullOrEmpty($parentPath) -or $parentPath -eq $currentPath) {
throw "Failed to locate repository root from script path: $StartPath"
}
$currentPath = $parentPath
}
}
$RepoRoot = Get-RepoRoot -StartPath $PSScriptRoot
Add-Type -TypeDefinition @"
using System;
public static class PodkopLmoHash
{
private static uint Get16(byte[] data, int offset)
{
return (uint)(data[offset] | (data[offset + 1] << 8));
}
public static uint Compute(byte[] data, uint init)
{
if (data == null || data.Length == 0)
return 0;
uint hash = init;
uint tmp;
int rem = data.Length & 3;
int len = data.Length >> 2;
int offset = 0;
while (len-- > 0)
{
hash += Get16(data, offset);
tmp = (Get16(data, offset + 2) << 11) ^ hash;
hash = (hash << 16) ^ tmp;
offset += 4;
hash += hash >> 11;
}
switch (rem)
{
case 3:
hash += Get16(data, offset);
hash ^= hash << 16;
hash ^= (uint)(((sbyte)data[offset + 2]) << 18);
hash += hash >> 11;
break;
case 2:
hash += Get16(data, offset);
hash ^= hash << 11;
hash += hash >> 17;
break;
case 1:
hash += (uint)(sbyte)data[offset];
hash ^= hash << 10;
hash += hash >> 1;
break;
}
hash ^= hash << 3;
hash += hash >> 5;
hash ^= hash << 4;
hash += hash >> 17;
hash ^= hash << 25;
hash += hash >> 6;
return hash;
}
}
"@
function Write-Step {
param([string]$Message)
Write-Host "==> $Message" -ForegroundColor Cyan
}
function Get-RequiredCommand {
param([string]$Name)
$command = Get-Command -Name $Name -ErrorAction SilentlyContinue
if (-not $command) {
throw "Required command '$Name' was not found in PATH."
}
return $command.Source
}
function Invoke-External {
param(
[string]$FilePath,
[string[]]$Arguments,
[string]$Description
)
if ($Description) {
Write-Step $Description
}
& $FilePath @Arguments
if ($LASTEXITCODE -ne 0) {
$argString = ($Arguments | ForEach-Object { "'$_'" }) -join ' '
throw "Command failed with exit code ${LASTEXITCODE}: $FilePath $argString"
}
}
function Invoke-ExternalCapture {
param(
[string]$FilePath,
[string[]]$Arguments
)
$output = & $FilePath @Arguments 2>&1
if ($LASTEXITCODE -ne 0) {
$argString = ($Arguments | ForEach-Object { "'$_'" }) -join ' '
throw "Command failed with exit code ${LASTEXITCODE}: $FilePath $argString`n$($output -join [Environment]::NewLine)"
}
return ($output -join [Environment]::NewLine).Trim()
}
function Assert-RepoPath {
param([string]$RelativePath)
$fullPath = Join-Path $RepoRoot $RelativePath
if (-not (Test-Path -LiteralPath $fullPath)) {
throw "Expected repository path is missing: $RelativePath"
}
return $fullPath
}
function Get-DeployVersion {
$makefilePath = Assert-RepoPath 'luci-app-podkop-plus\Makefile'
$makefile = Get-Content -LiteralPath $makefilePath -Raw -Encoding UTF8
$pkgVersion = 'dev'
if ($makefile -match 'PKG_VERSION:=.*?([0-9]+\.[0-9]+\.[0-9]+)') {
$pkgVersion = $Matches[1]
}
$git = Get-Command -Name git -ErrorAction SilentlyContinue
if ($git) {
try {
$gitShort = Invoke-ExternalCapture -FilePath $git.Source -Arguments @('-C', $RepoRoot, 'rev-parse', '--short', 'HEAD')
if ($gitShort) {
return "$pkgVersion-dev+$gitShort"
}
}
catch {
}
}
return "$pkgVersion-dev"
}
function Copy-Tree {
param(
[string]$Source,
[string]$Destination
)
New-Item -ItemType Directory -Path $Destination -Force | Out-Null
Get-ChildItem -LiteralPath $Source -Force | ForEach-Object {
Copy-Item -LiteralPath $_.FullName -Destination $Destination -Recurse -Force
}
}
function Write-Utf8NoBom {
param(
[string]$Path,
[string]$Content
)
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($Path, $Content, $utf8NoBom)
}
function Replace-VersionPlaceholder {
param(
[string]$Path,
[string]$Version
)
$content = Get-Content -LiteralPath $Path -Raw -Encoding UTF8
if ($content.Contains('__COMPILED_VERSION_VARIABLE__')) {
Write-Utf8NoBom -Path $Path -Content $content.Replace('__COMPILED_VERSION_VARIABLE__', $Version)
}
}
function New-PoMessageState {
return @{
PluralNum = -1
Ctxt = $null
Id = $null
IdPlural = $null
Values = (New-Object string[] 10)
CurrentField = $null
CurrentIndex = -1
}
}
function Get-PoStringSegment {
param([string]$Line)
if ([string]::IsNullOrEmpty($Line) -or $Line[0] -eq '#') {
return $null
}
$quoteIndex = $Line.IndexOf('"')
if ($quoteIndex -lt 0) {
return $null
}
$builder = New-Object System.Text.StringBuilder
$escape = $false
for ($index = $quoteIndex + 1; $index -lt $Line.Length; $index++) {
$char = $Line[$index]
if ($escape) {
if (($char -eq '"') -or ($char -eq '\')) {
[void]$builder.Remove($builder.Length - 1, 1)
}
[void]$builder.Append($char)
$escape = $false
continue
}
if ($char -eq '\') {
[void]$builder.Append($char)
$escape = $true
continue
}
if ($char -eq '"') {
break
}
[void]$builder.Append($char)
}
return $builder.ToString()
}
function Add-PoSegment {
param(
[hashtable]$Message,
[string]$Segment
)
if ([string]::IsNullOrEmpty($Segment)) {
return
}
switch ($Message.CurrentField) {
'ctxt' {
if ($null -eq $Message.Ctxt) {
$Message.Ctxt = ''
}
$Message.Ctxt += $Segment
}
'id' {
if ($null -eq $Message.Id) {
$Message.Id = ''
}
$Message.Id += $Segment
}
'idPlural' {
if ($null -eq $Message.IdPlural) {
$Message.IdPlural = ''
}
$Message.IdPlural += $Segment
}
'value' {
if ($Message.CurrentIndex -lt 0 -or $Message.CurrentIndex -ge $Message.Values.Length) {
throw "Invalid PO plural index: $($Message.CurrentIndex)"
}
if ($null -eq $Message.Values[$Message.CurrentIndex]) {
$Message.Values[$Message.CurrentIndex] = ''
}
$Message.Values[$Message.CurrentIndex] += $Segment
}
}
}
function Get-SignedByte {
param([byte]$Value)
if ($Value -ge 128) {
return [int]$Value - 256
}
return [int]$Value
}
function Get-UInt16Le {
param(
[byte[]]$Bytes,
[int]$Index
)
return [uint32]($Bytes[$Index] -bor ($Bytes[$Index + 1] -shl 8))
}
function Convert-ToUInt32 {
param([long]$Value)
return [uint32]($Value -band 0xffffffffL)
}
function Get-SfhHash {
param(
[byte[]]$Data,
[uint32]$Init
)
return [PodkopLmoHash]::Compute($Data, $Init)
}
function Write-UInt32BigEndian {
param(
[System.IO.BinaryWriter]$Writer,
[uint32]$Value
)
$bytes = [System.BitConverter]::GetBytes($Value)
if ([System.BitConverter]::IsLittleEndian) {
[Array]::Reverse($bytes)
}
$Writer.Write($bytes)
}
function Write-AlignedBytes {
param(
[System.IO.BinaryWriter]$Writer,
[byte[]]$Bytes
)
$Writer.Write($Bytes)
$padding = (4 - ($Bytes.Length % 4)) % 4
for ($index = 0; $index -lt $padding; $index++) {
$Writer.Write([byte]0)
}
}
function Add-LmoEntry {
param(
[System.Collections.Generic.List[object]]$Entries,
[System.IO.BinaryWriter]$DataWriter,
[System.IO.MemoryStream]$DataStream,
[uint32]$KeyId,
[uint32]$ValId,
[string]$Value
)
$utf8 = [System.Text.Encoding]::UTF8
$valueBytes = $utf8.GetBytes($Value)
$entry = [pscustomobject]@{
KeyId = $KeyId
ValId = $ValId
Offset = [uint32]$DataStream.Length
Length = [uint32]$valueBytes.Length
}
Write-AlignedBytes -Writer $DataWriter -Bytes $valueBytes
[void]$Entries.Add($entry)
}
function Flush-PoMessage {
param(
[hashtable]$Message,
[System.Collections.Generic.List[object]]$Entries,
[System.IO.BinaryWriter]$DataWriter,
[System.IO.MemoryStream]$DataStream
)
$utf8 = [System.Text.Encoding]::UTF8
if ($null -ne $Message.Id -and $null -ne $Message.Values[0]) {
$pluralLimit = if ($Message.PluralNum -lt 0) { 0 } else { $Message.PluralNum }
for ($index = 0; $index -le $pluralLimit; $index++) {
$value = $Message.Values[$index]
if ($null -eq $value) {
continue
}
if (($null -ne $Message.Ctxt) -and ($null -ne $Message.IdPlural)) {
$key = '{0}{1}{2}{3}{4}' -f $Message.Ctxt, [char]1, $Message.Id, [char]2, $index
}
elseif ($null -ne $Message.Ctxt) {
$key = '{0}{1}{2}' -f $Message.Ctxt, [char]1, $Message.Id
}
elseif ($null -ne $Message.IdPlural) {
$key = '{0}{1}{2}' -f $Message.Id, [char]2, $index
}
else {
$key = $Message.Id
}
$keyBytes = $utf8.GetBytes($key)
$valueBytes = $utf8.GetBytes($value)
$keyHash = Get-SfhHash -Data $keyBytes -Init ([uint32]$keyBytes.Length)
$valueHash = Get-SfhHash -Data $valueBytes -Init ([uint32]$valueBytes.Length)
if ($keyHash -eq $valueHash) {
continue
}
Add-LmoEntry -Entries $Entries -DataWriter $DataWriter -DataStream $DataStream -KeyId $keyHash -ValId ([uint32]($Message.PluralNum + 1)) -Value $value
}
}
elseif ($null -ne $Message.Values[0]) {
foreach ($field in ($Message.Values[0] -split '\\n')) {
if ($field.StartsWith('Plural-Forms: ')) {
$pluralDefinition = $field.Substring(14)
Add-LmoEntry -Entries $Entries -DataWriter $DataWriter -DataStream $DataStream -KeyId ([uint32]0) -ValId ([uint32]0) -Value $pluralDefinition
break
}
}
}
$Message.PluralNum = -1
$Message.Ctxt = $null
$Message.Id = $null
$Message.IdPlural = $null
$Message.Values = (New-Object string[] 10)
$Message.CurrentField = $null
$Message.CurrentIndex = -1
}
function Convert-PoToLmo {
param(
[string]$PoPath,
[string]$LmoPath
)
$entries = [System.Collections.Generic.List[object]]::new()
$dataStream = New-Object System.IO.MemoryStream
$dataWriter = New-Object System.IO.BinaryWriter($dataStream)
$message = New-PoMessageState
try {
foreach ($line in (Get-Content -LiteralPath $PoPath -Encoding UTF8)) {
if ($line.StartsWith('msgctxt "')) {
if (($null -ne $message.Id) -or ($null -ne $message.Values[0])) {
Flush-PoMessage -Message $message -Entries $entries -DataWriter $dataWriter -DataStream $dataStream
}
$message.CurrentField = 'ctxt'
$message.CurrentIndex = -1
$message.Ctxt = $null
}
elseif ($line.StartsWith('msgid "')) {
if (($null -ne $message.Id) -or ($null -ne $message.Values[0])) {
Flush-PoMessage -Message $message -Entries $entries -DataWriter $dataWriter -DataStream $dataStream
}
$message.CurrentField = 'id'
$message.CurrentIndex = -1
$message.Id = $null
}
elseif ($line.StartsWith('msgid_plural "')) {
$message.CurrentField = 'idPlural'
$message.CurrentIndex = -1
$message.IdPlural = $null
}
elseif ($line.StartsWith('msgstr "')) {
$message.PluralNum = 0
$message.CurrentField = 'value'
$message.CurrentIndex = 0
$message.Values[0] = $null
}
elseif ($line.StartsWith('msgstr[')) {
if ($line -notmatch '^msgstr\[(\d+)\]') {
throw "Failed to parse plural msgstr line: $line"
}
$pluralIndex = [int]$Matches[1]
if ($pluralIndex -ge $message.Values.Length) {
throw "Too many plural forms in $PoPath"
}
$message.PluralNum = $pluralIndex
$message.CurrentField = 'value'
$message.CurrentIndex = $pluralIndex
$message.Values[$pluralIndex] = $null
}
$segment = Get-PoStringSegment -Line $line
if ($null -ne $segment) {
Add-PoSegment -Message $message -Segment $segment
}
}
Flush-PoMessage -Message $message -Entries $entries -DataWriter $dataWriter -DataStream $dataStream
if ($dataStream.Length -le 0) {
throw "No translatable data was produced from $PoPath"
}
$indexStream = New-Object System.IO.MemoryStream
$indexWriter = New-Object System.IO.BinaryWriter($indexStream)
try {
foreach ($entry in ($entries | Sort-Object -Property KeyId)) {
Write-UInt32BigEndian -Writer $indexWriter -Value ([uint32]$entry.KeyId)
Write-UInt32BigEndian -Writer $indexWriter -Value ([uint32]$entry.ValId)
Write-UInt32BigEndian -Writer $indexWriter -Value ([uint32]$entry.Offset)
Write-UInt32BigEndian -Writer $indexWriter -Value ([uint32]$entry.Length)
}
$finalStream = New-Object System.IO.MemoryStream
$finalWriter = New-Object System.IO.BinaryWriter($finalStream)
try {
$finalWriter.Write($dataStream.ToArray())
$finalWriter.Write($indexStream.ToArray())
Write-UInt32BigEndian -Writer $finalWriter -Value ([uint32]$dataStream.Length)
[System.IO.File]::WriteAllBytes($LmoPath, $finalStream.ToArray())
}
finally {
$finalWriter.Dispose()
$finalStream.Dispose()
}
}
finally {
$indexWriter.Dispose()
$indexStream.Dispose()
}
}
finally {
$dataWriter.Dispose()
$dataStream.Dispose()
}
}
function Get-RemoteInstallScript {
return @'
#!/bin/sh
set -eu
BUNDLE_DIR="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
replace_file() {
src="$1"
dest="$2"
mode="$3"
tmp="${dest}.podkop-plus-new.$$"
mkdir -p "$(dirname "$dest")"
cp "$src" "$tmp"
chmod "$mode" "$tmp"
mv -f "$tmp" "$dest"
}
replace_dir() {
src="$1"
dest="$2"
base_dir="$(dirname "$dest")"
dir_name="$(basename "$dest")"
tmp="${base_dir}/.${dir_name}.podkop-plus-new.$$"
mkdir -p "$base_dir"
rm -rf "$tmp"
mkdir -p "$tmp"
(cd "$src" && tar -cf - .) | (cd "$tmp" && tar -xf -)
rm -rf "$dest"
mv "$tmp" "$dest"
}
if [ -x /etc/init.d/podkop ]; then
/etc/init.d/podkop running >/dev/null 2>&1 && /etc/init.d/podkop stop >/dev/null 2>&1 || true
/etc/init.d/podkop enabled >/dev/null 2>&1 && /etc/init.d/podkop disable >/dev/null 2>&1 || true
fi
[ -x /etc/init.d/podkop-plus ] && /etc/init.d/podkop-plus stop >/dev/null 2>&1 || true
replace_file "$BUNDLE_DIR/etc/init.d/podkop-plus" "/etc/init.d/podkop-plus" 0755
replace_file "$BUNDLE_DIR/usr/bin/podkop-plus" "/usr/bin/podkop-plus" 0755
replace_dir "$BUNDLE_DIR/usr/lib/podkop-plus" "/usr/lib/podkop-plus"
replace_dir "$BUNDLE_DIR/www/luci-static/resources/view/podkop_plus" "/www/luci-static/resources/view/podkop_plus"
replace_file "$BUNDLE_DIR/usr/share/luci/menu.d/luci-app-podkop-plus.json" "/usr/share/luci/menu.d/luci-app-podkop-plus.json" 0644
replace_file "$BUNDLE_DIR/usr/share/rpcd/acl.d/luci-app-podkop-plus.json" "/usr/share/rpcd/acl.d/luci-app-podkop-plus.json" 0644
replace_file "$BUNDLE_DIR/usr/lib/lua/luci/i18n/podkop_plus.ru.lmo" "/usr/lib/lua/luci/i18n/podkop_plus.ru.lmo" 0644
replace_file "$BUNDLE_DIR/etc/uci-defaults/50_luci-podkop-plus" "/etc/uci-defaults/50_luci-podkop-plus" 0755
is_original_podkop_present() {
opkg list-installed 2>/dev/null | grep -Eq '^podkop([[:space:]-]|$)' ||
opkg list-installed 2>/dev/null | grep -Eq '^luci-app-podkop([[:space:]-]|$)' ||
[ -x /etc/init.d/podkop ] ||
[ -x /usr/bin/podkop ] ||
[ -d /usr/lib/podkop ] ||
[ -f /usr/share/luci/menu.d/luci-app-podkop.json ] ||
[ -f /usr/share/rpcd/acl.d/luci-app-podkop.json ]
}
if [ ! -f /etc/config/podkop-plus ]; then
if [ -f /etc/config/podkop_plus ]; then
cp /etc/config/podkop_plus /etc/config/podkop-plus
chmod 0644 /etc/config/podkop-plus || true
elif [ -f /etc/config/podkop ] &&
{ [ -x /etc/init.d/podkop-plus ] || [ -x /usr/bin/podkop-plus ] || [ -d /usr/lib/podkop-plus ]; } &&
! is_original_podkop_present; then
cp /etc/config/podkop /etc/config/podkop-plus
chmod 0644 /etc/config/podkop-plus || true
else
replace_file "$BUNDLE_DIR/etc/config/podkop-plus" "/etc/config/podkop-plus" 0644
fi
fi
rm -f /var/luci-indexcache* /tmp/luci-indexcache*
[ -x /etc/init.d/rpcd ] && /etc/init.d/rpcd reload >/dev/null 2>&1 || true
/etc/init.d/podkop-plus enable >/dev/null 2>&1 || true
get_status_json() {
/usr/bin/podkop-plus get_status 2>/dev/null || true
}
wait_for_service() {
attempts=90
consecutive=0
LAST_STATUS_JSON='{}'
while [ "$attempts" -gt 0 ]; do
LAST_STATUS_JSON="$(get_status_json)"
if printf '%s\n' "$LAST_STATUS_JSON" | grep -q '"running":1'; then
consecutive=$((consecutive + 1))
if [ "$consecutive" -ge 3 ]; then
return 0
fi
else
consecutive=0
fi
attempts=$((attempts - 1))
sleep 1
done
return 1
}
/etc/init.d/podkop-plus restart >/dev/null 2>&1 || true
echo "System LuCI language: $(uci -q get luci.main.lang || echo auto)"
echo "Podkop Plus RU catalog: $( [ -f /usr/lib/lua/luci/i18n/podkop_plus.ru.lmo ] && echo installed || echo absent )"
echo "Autostart enabled: $(/etc/init.d/podkop-plus enabled >/dev/null 2>&1 && echo yes || echo no)"
echo "Service status:"
if ! wait_for_service; then
printf '%s\n' "$LAST_STATUS_JSON"
exit 1
fi
printf '%s\n' "$LAST_STATUS_JSON"
'@
}
function New-DeployBundle {
param(
[string]$StageRoot,
[string]$Version
)
$bundleFiles = @{
'podkop/files/etc/init.d/podkop' = 'etc/init.d/podkop-plus'
'podkop/files/etc/config/podkop' = 'etc/config/podkop-plus'
'podkop/files/usr/bin/podkop' = 'usr/bin/podkop-plus'
'luci-app-podkop-plus/root/usr/share/luci/menu.d/luci-app-podkop-plus.json' = 'usr/share/luci/menu.d/luci-app-podkop-plus.json'
'luci-app-podkop-plus/root/usr/share/rpcd/acl.d/luci-app-podkop-plus.json' = 'usr/share/rpcd/acl.d/luci-app-podkop-plus.json'
'luci-app-podkop-plus/root/etc/uci-defaults/50_luci-podkop-plus' = 'etc/uci-defaults/50_luci-podkop-plus'
}
foreach ($entry in $bundleFiles.GetEnumerator()) {
$source = Assert-RepoPath $entry.Key
$destination = Join-Path $StageRoot $entry.Value
New-Item -ItemType Directory -Path (Split-Path -Parent $destination) -Force | Out-Null
Copy-Item -LiteralPath $source -Destination $destination -Force
}
Copy-Tree -Source (Assert-RepoPath 'podkop/files/usr/lib') -Destination (Join-Path $StageRoot 'usr/lib/podkop-plus')
Copy-Tree -Source (Assert-RepoPath 'luci-app-podkop-plus/htdocs/luci-static/resources/view/podkop') -Destination (Join-Path $StageRoot 'www/luci-static/resources/view/podkop_plus')
Replace-VersionPlaceholder -Path (Join-Path $StageRoot 'usr/lib/podkop-plus/constants.sh') -Version $Version
$mainJsPath = Join-Path $StageRoot 'www/luci-static/resources/view/podkop_plus/main.js'
if (Test-Path -LiteralPath $mainJsPath) {
Replace-VersionPlaceholder -Path $mainJsPath -Version $Version
}
$poPath = Assert-RepoPath 'luci-app-podkop-plus\po\ru\podkop_plus.po'
$lmoPath = Join-Path $StageRoot 'usr/lib/lua/luci/i18n/podkop_plus.ru.lmo'
New-Item -ItemType Directory -Path (Split-Path -Parent $lmoPath) -Force | Out-Null
Convert-PoToLmo -PoPath $poPath -LmoPath $lmoPath
$remoteScriptPath = Join-Path $StageRoot '.devdeploy\install.sh'
New-Item -ItemType Directory -Path (Split-Path -Parent $remoteScriptPath) -Force | Out-Null
Write-Utf8NoBom -Path $remoteScriptPath -Content (Get-RemoteInstallScript)
}
function New-TarArchive {
param(
[string]$TarExe,
[string]$SourceDirectory,
[string]$ArchivePath
)
if (Test-Path -LiteralPath $ArchivePath) {
Remove-Item -LiteralPath $ArchivePath -Force
}
Push-Location $SourceDirectory
try {
Invoke-External -FilePath $TarExe -Arguments @('-czf', $ArchivePath, '.') -Description 'Packing deploy bundle'
}
finally {
Pop-Location
}
}
$sshExe = Get-RequiredCommand -Name 'ssh.exe'
$scpExe = Get-RequiredCommand -Name 'scp.exe'
$tarExe = Get-RequiredCommand -Name 'tar.exe'
[void](Assert-RepoPath 'podkop')
[void](Assert-RepoPath 'luci-app-podkop-plus')
$version = Get-DeployVersion
$target = "$User@$Router"
$sshArgs = @('-p', $Port.ToString(), '-o', 'BatchMode=yes')
$scpArgs = @('-O', '-P', $Port.ToString(), '-o', 'BatchMode=yes')
$remoteId = "podkop-plus-devdeploy-$PID"
$remoteArchive = "/tmp/$remoteId.tgz"
$remoteWorkDir = "/tmp/$remoteId"
$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) "$remoteId-local"
$stageRoot = Join-Path $tempRoot 'bundle'
$archivePath = Join-Path $tempRoot "$remoteId.tgz"
try {
if (Test-Path -LiteralPath $tempRoot) {
Remove-Item -LiteralPath $tempRoot -Recurse -Force
}
New-Item -ItemType Directory -Path $stageRoot -Force | Out-Null
Write-Step "Preparing deploy bundle for $target"
New-DeployBundle -StageRoot $stageRoot -Version $version
New-TarArchive -TarExe $tarExe -SourceDirectory $stageRoot -ArchivePath $archivePath
Invoke-External -FilePath $scpExe -Arguments ($scpArgs + @($archivePath, "${target}:$remoteArchive")) -Description "Uploading archive to $target"
$remoteCommand = @'
set -eu
archive='__ARCHIVE__'
work='__WORKDIR__'
cleanup() {
rm -rf "$work" "$archive"
}
trap cleanup EXIT INT TERM
rm -rf "$work"
mkdir -p "$work"
tar -xzf "$archive" -C "$work"
sh "$work/.devdeploy/install.sh"
'@
$remoteCommand = $remoteCommand.Replace('__ARCHIVE__', $remoteArchive)
$remoteCommand = $remoteCommand.Replace('__WORKDIR__', $remoteWorkDir)
Invoke-External -FilePath $sshExe -Arguments ($sshArgs + @($target, $remoteCommand)) -Description "Installing bundle on $target"
Write-Step 'Deploy finished successfully'
}
finally {
if (Test-Path -LiteralPath $tempRoot) {
Remove-Item -LiteralPath $tempRoot -Recurse -Force
}
}