ci(release): test and package five native platforms in one workflow
This commit is contained in:
1 parent
e239fff253
commit
8934d566c1
3 files changed
+106
-24
No files matched your search
@@ -55,18 +55,39 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: web/dist-demo
|
path: web/dist-demo
|
||||||
|
|
||||||
linux:
|
native:
|
||||||
needs: check
|
needs: check
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- arch: amd64
|
- os: linux
|
||||||
|
arch: amd64
|
||||||
runner: ubuntu-24.04
|
runner: ubuntu-24.04
|
||||||
- arch: arm64
|
extension: ''
|
||||||
|
- os: linux
|
||||||
|
arch: arm64
|
||||||
runner: ubuntu-24.04-arm
|
runner: ubuntu-24.04-arm
|
||||||
|
extension: ''
|
||||||
|
- os: windows
|
||||||
|
arch: amd64
|
||||||
|
runner: windows-2025
|
||||||
|
extension: '.exe'
|
||||||
|
- os: darwin
|
||||||
|
arch: arm64
|
||||||
|
runner: macos-15
|
||||||
|
extension: ''
|
||||||
|
- os: darwin
|
||||||
|
arch: amd64
|
||||||
|
runner: macos-15-intel
|
||||||
|
extension: ''
|
||||||
runs-on: ${{ matrix.runner }}
|
runs-on: ${{ matrix.runner }}
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: '0'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||||
@@ -77,30 +98,34 @@ jobs:
|
|||||||
name: frontend
|
name: frontend
|
||||||
path: web/dist
|
path: web/dist
|
||||||
- name: Build and smoke-test native binary
|
- name: Build and smoke-test native binary
|
||||||
|
env:
|
||||||
|
EXE: ${{ matrix.extension }}
|
||||||
run: |
|
run: |
|
||||||
mkdir -p package/licenses
|
mkdir -p package/licenses
|
||||||
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.releaseVersion=$(cat VERSION)" -o package/rawmark ./cmd/rawmark
|
go build -trimpath -ldflags="-s -w -X main.releaseVersion=$(cat VERSION)" -o "package/rawmark$EXE" ./cmd/rawmark
|
||||||
go test ./cmd/rawmark ./web
|
go vet ./...
|
||||||
python3 scripts/smoke-release.py package/rawmark
|
go test ./internal/... ./cmd/rawmark ./web
|
||||||
|
python scripts/smoke-release.py "package/rawmark$EXE"
|
||||||
- name: Package download
|
- name: Package download
|
||||||
env:
|
env:
|
||||||
ARCH: ${{ matrix.arch }}
|
ARCH: ${{ matrix.arch }}
|
||||||
|
TARGET_OS: ${{ matrix.os }}
|
||||||
run: |
|
run: |
|
||||||
cp LICENSE package/
|
cp LICENSE package/
|
||||||
cp web/LICENSE package/licenses/vue3-starter-MIT.txt
|
cp web/LICENSE package/licenses/vue3-starter-MIT.txt
|
||||||
cp web/src/common/appearance/fonts/*-OFL.txt package/licenses/
|
cp web/src/common/appearance/fonts/*-OFL.txt package/licenses/
|
||||||
cp web/src/common/appearance/fonts/maple/LICENSE.txt package/licenses/Maple-Mono-OFL.txt
|
cp web/src/common/appearance/fonts/maple/LICENSE.txt package/licenses/Maple-Mono-OFL.txt
|
||||||
mkdir downloads
|
mkdir downloads
|
||||||
tar -C package -czf "downloads/rawmark_$(cat VERSION)_linux_$ARCH.tar.gz" rawmark LICENSE licenses
|
python scripts/package-release.py "$TARGET_OS" "$ARCH"
|
||||||
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||||
with:
|
with:
|
||||||
name: linux-${{ matrix.arch }}
|
name: native-${{ matrix.os }}-${{ matrix.arch }}
|
||||||
path: downloads/*.tar.gz
|
path: downloads/*
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 3
|
retention-days: 3
|
||||||
|
|
||||||
image:
|
image:
|
||||||
needs: linux
|
needs: native
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
permissions:
|
permissions:
|
||||||
@@ -112,7 +137,7 @@ jobs:
|
|||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||||
with:
|
with:
|
||||||
pattern: linux-*
|
pattern: native-linux-*
|
||||||
path: downloads
|
path: downloads
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
- name: Prepare tested binaries
|
- name: Prepare tested binaries
|
||||||
@@ -186,7 +211,7 @@ jobs:
|
|||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||||
with:
|
with:
|
||||||
pattern: linux-*
|
pattern: native-*
|
||||||
path: downloads
|
path: downloads
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||||
@@ -201,6 +226,6 @@ jobs:
|
|||||||
RELEASE_TAG: ${{ github.ref_name }}
|
RELEASE_TAG: ${{ github.ref_name }}
|
||||||
IMAGE_DIGEST: ${{ needs.image.outputs.digest }}
|
IMAGE_DIGEST: ${{ needs.image.outputs.digest }}
|
||||||
run: |
|
run: |
|
||||||
(cd downloads && sha256sum *.tar.gz > SHA256SUMS)
|
(cd downloads && sha256sum *.tar.gz *.zip > SHA256SUMS)
|
||||||
gh release create "$RELEASE_TAG" downloads/* --verify-tag --title "RawMark $RELEASE_TAG" --notes-file .github/release-notes.md --latest
|
gh release create "$RELEASE_TAG" downloads/* --verify-tag --title "RawMark $RELEASE_TAG" --notes-file .github/release-notes.md --latest
|
||||||
docker buildx imagetools create --tag "ghcr.io/eezd/rawmark:$(cat VERSION)" --tag ghcr.io/eezd/rawmark:latest "ghcr.io/eezd/rawmark@$IMAGE_DIGEST"
|
docker buildx imagetools create --tag "ghcr.io/eezd/rawmark:$(cat VERSION)" --tag ghcr.io/eezd/rawmark:latest "ghcr.io/eezd/rawmark@$IMAGE_DIGEST"
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Package the already-tested native executable and licenses for a release."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import tarfile
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
system, arch = sys.argv[1:]
|
||||||
|
version = Path('VERSION').read_text().strip()
|
||||||
|
name = f'rawmark_{version}_{system}_{arch}'
|
||||||
|
package = Path('package')
|
||||||
|
files = sorted(p for p in package.rglob('*') if p.is_file())
|
||||||
|
if system == 'windows':
|
||||||
|
with zipfile.ZipFile(f'downloads/{name}.zip', 'w', compression=zipfile.ZIP_DEFLATED) as archive:
|
||||||
|
for file in files:
|
||||||
|
archive.write(file, file.relative_to(package).as_posix())
|
||||||
|
else:
|
||||||
|
with tarfile.open(f'downloads/{name}.tar.gz', 'w:gz') as archive:
|
||||||
|
for file in files:
|
||||||
|
archive.add(file, arcname=file.relative_to(package).as_posix())
|
||||||
+47
-11
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Exercise a release binary using disposable data only, on either Linux architecture."""
|
"""Exercise a release binary using disposable data only, on every supported native platform."""
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -23,7 +23,7 @@ assert subprocess.check_output([binary, '--version'], text=True).strip() == f'Ra
|
|||||||
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory(prefix='rawmark-release-') as directory:
|
with tempfile.TemporaryDirectory(prefix='rawmark-release-') as directory:
|
||||||
data = Path(directory) / 'data'
|
data = Path(directory) / '数据 data'
|
||||||
with socket.socket() as listener:
|
with socket.socket() as listener:
|
||||||
listener.bind(('127.0.0.1', 0))
|
listener.bind(('127.0.0.1', 0))
|
||||||
port = listener.getsockname()[1]
|
port = listener.getsockname()[1]
|
||||||
@@ -31,11 +31,11 @@ with tempfile.TemporaryDirectory(prefix='rawmark-release-') as directory:
|
|||||||
environment = {**os.environ, 'RAWMARK_DATA': str(data),
|
environment = {**os.environ, 'RAWMARK_DATA': str(data),
|
||||||
'RAWMARK_VAULT': str(data / 'vault'), 'RAWMARK_ADDR': f'127.0.0.1:{port}'}
|
'RAWMARK_VAULT': str(data / 'vault'), 'RAWMARK_ADDR': f'127.0.0.1:{port}'}
|
||||||
|
|
||||||
def request(path, *, method='GET', body=None, token=None, status=200):
|
def request(path, *, method='GET', body=None, token=None, status=200, raw=False, headers=None):
|
||||||
headers = {'Accept-Language': 'en'}
|
headers = {'Accept-Language': 'en', **(headers or {})}
|
||||||
if token:
|
if token:
|
||||||
headers['Authorization'] = 'Bearer ' + token
|
headers['Authorization'] = 'Bearer ' + token
|
||||||
if body is not None:
|
if body is not None and not isinstance(body, bytes):
|
||||||
headers['Content-Type'] = 'application/json'
|
headers['Content-Type'] = 'application/json'
|
||||||
body = json.dumps(body).encode()
|
body = json.dumps(body).encode()
|
||||||
req = urllib.request.Request(origin + '/api/v1/' + path, data=body, headers=headers, method=method)
|
req = urllib.request.Request(origin + '/api/v1/' + path, data=body, headers=headers, method=method)
|
||||||
@@ -45,13 +45,15 @@ with tempfile.TemporaryDirectory(prefix='rawmark-release-') as directory:
|
|||||||
response = error
|
response = error
|
||||||
with response:
|
with response:
|
||||||
assert response.status == status, (path, response.status, response.read())
|
assert response.status == status, (path, response.status, response.read())
|
||||||
|
if raw:
|
||||||
|
return response.read()
|
||||||
result = json.load(response)
|
result = json.load(response)
|
||||||
assert (result['code'] == 0) == (status < 400), result
|
assert (result['code'] == 0) == (status < 400), result
|
||||||
return result['data']
|
return result['data']
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def running():
|
def running():
|
||||||
with (Path(directory) / 'server.log').open('w+') as log:
|
with (Path(directory) / 'server.log').open('w+', encoding='utf-8') as log:
|
||||||
process = subprocess.Popen([binary], env=environment, stdout=log, stderr=log)
|
process = subprocess.Popen([binary], env=environment, stdout=log, stderr=log)
|
||||||
try:
|
try:
|
||||||
for _ in range(100):
|
for _ in range(100):
|
||||||
@@ -86,10 +88,18 @@ with tempfile.TemporaryDirectory(prefix='rawmark-release-') as directory:
|
|||||||
assert (data / 'vault' / note.relative_to('deploy/data/vault')).read_bytes() == note.read_bytes()
|
assert (data / 'vault' / note.relative_to('deploy/data/vault')).read_bytes() == note.read_bytes()
|
||||||
path = urllib.parse.quote('Getting Started/Welcome to RawMark.md')
|
path = urllib.parse.quote('Getting Started/Welcome to RawMark.md')
|
||||||
original = (data / 'vault/Getting Started/Welcome to RawMark.md').read_bytes()
|
original = (data / 'vault/Getting Started/Welcome to RawMark.md').read_bytes()
|
||||||
req = urllib.request.Request(origin + '/api/v1/files/' + path, data=original + b'\nRelease smoke test.\n',
|
request('files/' + path, method='PUT', body=original + b'\nRelease smoke test.\n', token=token)
|
||||||
headers={'Authorization': 'Bearer ' + token}, method='PUT')
|
note = '中文 folder/笔记 note.md'
|
||||||
with urllib.request.urlopen(req, timeout=5) as response:
|
moved = '移动 folder/新笔记 note.md'
|
||||||
assert json.load(response)['code'] == 0
|
content = '# Portable note\n\n中文 content.\n'.encode()
|
||||||
|
request('files/' + urllib.parse.quote(note), method='PUT', body=content, token=token)
|
||||||
|
request('files/' + urllib.parse.quote(note), method='PUT', body=content + b'Updated.\n', token=token)
|
||||||
|
request('move', method='POST', body={'from': note, 'to': moved}, token=token)
|
||||||
|
assert not (data / 'vault' / note).exists()
|
||||||
|
item = request('files/' + urllib.parse.quote(moved), method='DELETE', token=token)
|
||||||
|
assert item['id'] in [entry['id'] for entry in request('trash', token=token)['items']]
|
||||||
|
request('trash/' + item['id'] + '/restore', method='POST', token=token)
|
||||||
|
assert (data / 'vault' / moved).read_bytes() == content + b'Updated.\n'
|
||||||
|
|
||||||
before = {str(p.relative_to(data / 'vault')): hashlib.sha256(p.read_bytes()).hexdigest()
|
before = {str(p.relative_to(data / 'vault')): hashlib.sha256(p.read_bytes()).hexdigest()
|
||||||
for p in (data / 'vault').rglob('*') if p.is_file()}
|
for p in (data / 'vault').rglob('*') if p.is_file()}
|
||||||
@@ -99,4 +109,30 @@ with tempfile.TemporaryDirectory(prefix='rawmark-release-') as directory:
|
|||||||
after = {str(p.relative_to(data / 'vault')): hashlib.sha256(p.read_bytes()).hexdigest()
|
after = {str(p.relative_to(data / 'vault')): hashlib.sha256(p.read_bytes()).hexdigest()
|
||||||
for p in (data / 'vault').rglob('*') if p.is_file()}
|
for p in (data / 'vault').rglob('*') if p.is_file()}
|
||||||
assert before == after, 'Restart modified existing notes'
|
assert before == after, 'Restart modified existing notes'
|
||||||
print(f'RawMark {version}: initialization, authentication, examples, editing and restart passed')
|
archive = request('maintenance/export/full', method='POST', token=token, raw=True)
|
||||||
|
restored_content = (data / 'vault' / moved).read_bytes()
|
||||||
|
request('files/' + urllib.parse.quote(moved), method='PUT', body=b'Changed after backup', token=token)
|
||||||
|
upload = request('maintenance/uploads', method='POST', body={'kind': 'full', 'size': len(archive)},
|
||||||
|
token=token, status=201)['id']
|
||||||
|
for offset in range(0, len(archive), 8 << 20):
|
||||||
|
chunk = archive[offset:offset + (8 << 20)]
|
||||||
|
result = request('maintenance/uploads/' + upload, method='PUT', body=chunk, token=token,
|
||||||
|
headers={'Upload-Offset': str(offset)})
|
||||||
|
assert result['offset'] == offset + len(chunk)
|
||||||
|
request('maintenance/uploads/' + upload + '/validate', method='POST', token=token)
|
||||||
|
request('maintenance/restore', method='POST', body={'id': upload, 'confirmed': True}, token=token, status=202)
|
||||||
|
for _ in range(200):
|
||||||
|
if ((data / 'vault' / moved).is_file()
|
||||||
|
and (data / 'vault' / moved).read_bytes() == restored_content
|
||||||
|
and not (data / '.maintenance/restore.json').exists()):
|
||||||
|
try:
|
||||||
|
request('health')
|
||||||
|
break
|
||||||
|
except urllib.error.URLError:
|
||||||
|
pass
|
||||||
|
time.sleep(0.1)
|
||||||
|
else:
|
||||||
|
raise TimeoutError('Full restore did not complete')
|
||||||
|
request('auth/me', token=token, status=401)
|
||||||
|
print(f'RawMark {version}: initialization, authentication, examples, overwrite, Unicode paths, '
|
||||||
|
'move, recycle bin, restart and full backup/restore passed')
|
||||||
Reference in new issue
Block a user