Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 109 additions & 22 deletions .github/workflows/gh-akf-new-release.yaml
Original file line number Diff line number Diff line change
@@ -1,24 +1,48 @@
name: Publish GitHub release

on:
workflow_dispatch:
inputs:
release_tag:
description: Existing frozen source tag to package; never moves a tag.
required: true
type: string
default: v1.0.10
runtime_artifact_run:
description: Optional completed tagged-push run whose successful runtime archives can be reused.
required: false
type: string
push:
tags:
- "v*.*.*"

permissions:
contents: write
actions: read

env:
AKOFLOW_RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }}

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: ${{ github.workflow }}-${{ inputs.release_tag || github.ref_name }}
cancel-in-progress: true

jobs:
test:
name: Test daemon
runs-on: ubuntu-latest
steps:
- name: Validate frozen release tag
shell: bash
run: |
if ! [[ "$AKOFLOW_RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-.+][0-9A-Za-z.-]+)?$ ]]; then
echo "A semantic version source tag is required." >&2
exit 1
fi
- name: Checkout source
uses: actions/checkout@v6
with:
ref: refs/tags/${{ env.AKOFLOW_RELEASE_TAG }}

- name: Set up Go
uses: actions/setup-go@v6
Expand Down Expand Up @@ -46,56 +70,100 @@ jobs:
steps:
- name: Checkout source
uses: actions/checkout@v6
with:
ref: refs/tags/${{ env.AKOFLOW_RELEASE_TAG }}

- name: Verify runtime artifact provenance
id: reuse
if: inputs.runtime_artifact_run != ''
shell: bash
env:
GH_TOKEN: ${{ github.token }}
AKOFLOW_RUNTIME_RUN: ${{ inputs.runtime_artifact_run }}
AKOFLOW_RUNTIME_ARCH: ${{ matrix.arch }}
run: |
set -euo pipefail
if ! [[ "$AKOFLOW_RUNTIME_RUN" =~ ^[0-9]+$ ]]; then
echo "Runtime artifact run must be numeric." >&2
exit 1
fi
source_sha=$(git rev-parse HEAD)
gh api "repos/$GITHUB_REPOSITORY/actions/runs/$AKOFLOW_RUNTIME_RUN" |
jq --exit-status --arg sha "$source_sha" --arg repo "$GITHUB_REPOSITORY" --arg tag "$AKOFLOW_RELEASE_TAG" \
'.head_sha == $sha and .status == "completed" and .event == "push" and .head_branch == $tag and .head_repository.full_name == $repo' >/dev/null
gh api "repos/$GITHUB_REPOSITORY/actions/runs/$AKOFLOW_RUNTIME_RUN/jobs" |
jq --exit-status --arg name "Build runtime archives ($AKOFLOW_RUNTIME_ARCH)" \
'[.jobs[] | select(.name == $name and .conclusion == "success")] | length == 1' >/dev/null
echo "enabled=true" >> "$GITHUB_OUTPUT"

- name: Download verified runtime archives
if: steps.reuse.outputs.enabled == 'true'
uses: actions/download-artifact@v5
with:
name: runtime-${{ matrix.arch }}
run-id: ${{ inputs.runtime_artifact_run }}
github-token: ${{ github.token }}
path: /tmp

- name: Set up Docker Buildx
if: steps.reuse.outputs.enabled != 'true'
uses: docker/setup-buildx-action@v3

- name: Build daemon archive
if: steps.reuse.outputs.enabled != 'true'
uses: docker/build-push-action@v6
with:
context: .
file: releases/dockerfiles/server.Dockerfile
platforms: ${{ matrix.platform }}
push: false
tags: akoflow/daemon:${{ github.ref_name }}
outputs: type=docker,dest=/tmp/akoflow-daemon-${{ github.ref_name }}-linux-${{ matrix.arch }}.tar
tags: akoflow/daemon:${{ env.AKOFLOW_RELEASE_TAG }}
outputs: type=docker,dest=/tmp/akoflow-daemon-${{ env.AKOFLOW_RELEASE_TAG }}-linux-${{ matrix.arch }}.tar
build-args: |
AKOFLOW_VERSION=${{ github.ref_name }}
AKOFLOW_VERSION=${{ env.AKOFLOW_RELEASE_TAG }}
cache-from: type=gha,scope=runtime-daemon-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=runtime-daemon-${{ matrix.arch }}

- name: Build BuildKit archive
if: steps.reuse.outputs.enabled != 'true'
uses: docker/build-push-action@v6
with:
context: .
file: releases/dockerfiles/buildkit.Dockerfile
platforms: ${{ matrix.platform }}
push: false
tags: akoflow/buildkit:${{ github.ref_name }}
outputs: type=docker,dest=/tmp/akoflow-buildkit-${{ github.ref_name }}-linux-${{ matrix.arch }}.tar
tags: akoflow/buildkit:${{ env.AKOFLOW_RELEASE_TAG }}
outputs: type=docker,dest=/tmp/akoflow-buildkit-${{ env.AKOFLOW_RELEASE_TAG }}-linux-${{ matrix.arch }}.tar
build-args: |
AKOFLOW_VERSION=${{ github.ref_name }}
AKOFLOW_VERSION=${{ env.AKOFLOW_RELEASE_TAG }}
cache-from: type=gha,scope=runtime-buildkit-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=runtime-buildkit-${{ matrix.arch }}

- name: Create runtime checksums
if: steps.reuse.outputs.enabled != 'true'
shell: bash
run: |
set -euo pipefail
cd /tmp
sha256sum \
"akoflow-daemon-${GITHUB_REF_NAME}-linux-${{ matrix.arch }}.tar" \
"akoflow-buildkit-${GITHUB_REF_NAME}-linux-${{ matrix.arch }}.tar" \
> "akoflow-runtime-${GITHUB_REF_NAME}-linux-${{ matrix.arch }}.sha256"
"akoflow-daemon-${AKOFLOW_RELEASE_TAG}-linux-${{ matrix.arch }}.tar" \
"akoflow-buildkit-${AKOFLOW_RELEASE_TAG}-linux-${{ matrix.arch }}.tar" \
> "akoflow-runtime-${AKOFLOW_RELEASE_TAG}-linux-${{ matrix.arch }}.sha256"

- name: Verify runtime archive checksums
shell: bash
run: |
cd /tmp
sha256sum --check "akoflow-runtime-${AKOFLOW_RELEASE_TAG}-linux-${{ matrix.arch }}.sha256"

- name: Upload runtime archives
uses: actions/upload-artifact@v5
with:
name: runtime-${{ matrix.arch }}
path: |
/tmp/akoflow-daemon-${{ github.ref_name }}-linux-${{ matrix.arch }}.tar
/tmp/akoflow-buildkit-${{ github.ref_name }}-linux-${{ matrix.arch }}.tar
/tmp/akoflow-runtime-${{ github.ref_name }}-linux-${{ matrix.arch }}.sha256
/tmp/akoflow-daemon-${{ env.AKOFLOW_RELEASE_TAG }}-linux-${{ matrix.arch }}.tar
/tmp/akoflow-buildkit-${{ env.AKOFLOW_RELEASE_TAG }}-linux-${{ matrix.arch }}.tar
/tmp/akoflow-runtime-${{ env.AKOFLOW_RELEASE_TAG }}-linux-${{ matrix.arch }}.sha256
if-no-files-found: error

desktop:
Expand All @@ -114,7 +182,7 @@ jobs:
# fail while producing the universal macOS package.
signing: none
- runner: windows-latest
command: npx electron-builder --win --x64 --publish never
command: npx electron-builder --win --x64 --publish never --config.nsis.artifactName='Akoflow-Desktop-${version}-win-${arch}-setup.${ext}' --config.portable.artifactName='Akoflow-Desktop-${version}-win-${arch}-portable.${ext}'
artifact: windows-x64
signing: windows
- runner: ubuntu-latest
Expand All @@ -127,12 +195,24 @@ jobs:
uses: actions/checkout@v6
with:
repository: UFFeScience/akoflow-desktop
# Desktop releases intentionally package the current frontend main.
ref: main
# Application source is immutable; packaging overrides are explicit.
ref: refs/tags/${{ env.AKOFLOW_RELEASE_TAG }}
# GITHUB_TOKEN is scoped to this repository and cannot read the
# private desktop repository. Use a fine-grained cross-repo token.
token: ${{ secrets.DESKTOP_REPOSITORY_TOKEN }}

- name: Checkout release verification tools
uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
path: .release-tools

- name: Set up Go verification tools
uses: actions/setup-go@v6
with:
go-version-file: .release-tools/go.mod
cache: false

- name: Set up Node.js
uses: actions/setup-node@v6
with:
Expand All @@ -144,9 +224,9 @@ jobs:
id: version
shell: bash
run: |
version="${GITHUB_REF_NAME#v}"
version="${AKOFLOW_RELEASE_TAG#v}"
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-.+][0-9A-Za-z.-]+)?$ ]]; then
echo "Release tag must be a semantic version prefixed with v; received: $GITHUB_REF_NAME" >&2
echo "Release tag must be a semantic version prefixed with v." >&2
exit 1
fi
echo "value=$version" >> "$GITHUB_OUTPUT"
Expand Down Expand Up @@ -179,6 +259,11 @@ jobs:
fi
done

- name: Verify updater manifests against distributed files
working-directory: .release-tools
shell: bash
run: go run ./cmd/release-assets-check --dir "$GITHUB_WORKSPACE/release" --version "$AKOFLOW_RELEASE_TAG"

- name: Upload desktop package
uses: actions/upload-artifact@v5
with:
Expand All @@ -189,12 +274,12 @@ jobs:
release/*.exe
release/*.AppImage
release/*.deb
release/*.yml
release/latest*.yml
release/*.blockmap
if-no-files-found: error

release:
name: Create GitHub release
name: Create verified GitHub release draft
needs: [runtime-assets, desktop]
runs-on: ubuntu-latest
steps:
Expand Down Expand Up @@ -234,8 +319,10 @@ jobs:
- name: Create AkôFlow release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.AKOFLOW_RELEASE_TAG }}
draft: true
generate_release_notes: true
prerelease: ${{ contains(github.ref_name, '-') }}
prerelease: ${{ contains(env.AKOFLOW_RELEASE_TAG, '-') }}
files: release-assets/*
body: |
## Install AkôFlow Desktop
Expand All @@ -246,4 +333,4 @@ jobs:
- **Windows:** run the x64 portable `.exe` asset without an installation wizard.
- **Linux:** run the x64 `.AppImage` after `chmod +x`, or install the `.deb` with `sudo apt install ./Akoflow-Desktop-*.deb`.

The matching source is the Git tag `${{ github.ref_name }}`. The runtime Docker archives in this release are loaded locally by Desktop; AkôFlow does not publish daemon or BuildKit packages to a container registry.
The application source is the frozen Git tag `${{ env.AKOFLOW_RELEASE_TAG }}`. Windows packaging uses distinct `setup` and `portable` filenames and all updater SHA512 manifests are verified before upload. The runtime Docker archives in this release are loaded locally by Desktop; AkôFlow does not publish daemon or BuildKit packages to a container registry. This draft must be reviewed before publication.
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,33 @@ workflows, and distributed computing.

<a href="https://github.com/UFFeScience/akoflow/graphs/contributors"><img src="https://contrib.rocks/image?repo=UFFeScience/akoflow" alt="AkôFlow contributor avatars" /></a>

## Maintainer release packaging

Create the matching version tag in the Desktop repository before triggering the
backend release workflow. Application and runtime builds check out the frozen
source tag; packaging fixes can rebuild an existing draft without moving tags:

```sh
gh workflow run gh-akf-new-release.yaml \
--repo UFFeScience/akoflow --ref main \
--field release_tag=v1.0.10
```

The optional `runtime_artifact_run` input can reuse successful runtime archives
from a completed tagged-push run only when its repository, tag, source SHA and
architecture job all match. Downloaded archives are checked against their
SHA-256 manifests before upload. Desktop packages are always rebuilt and their
updater metadata verified; a failed publication does not justify reusing an
unverified installer.

Windows NSIS and portable executables use distinct `setup` and `portable`
filenames. Before upload, the Go `release-assets-check` command verifies updater
manifest versions, local filenames, sizes, SHA-512 values and legacy metadata
against the actual packages. The workflow creates a draft, not a public release.
Review successful checks, runtime checksums, package contents and screenshot
provenance before publishing the draft. Do not move an existing source tag to
hide a packaging correction.

## Cite AkôFlow

If AkôFlow contributes to your research, please cite:
Expand Down
41 changes: 41 additions & 0 deletions cmd/release-assets-check/checksum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package main

import (
"bytes"
"crypto/sha512"
"encoding/base64"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)

func verifyFile(directory string, artifact releaseFile) error {
if artifact.URL == "" || artifact.URL == "." || filepath.Base(artifact.URL) != artifact.URL || strings.ContainsAny(artifact.URL, "/\\") {
return fmt.Errorf("artifact must be a local basename: %q", artifact.URL)
}
want, err := base64.StdEncoding.DecodeString(artifact.SHA512)
if err != nil || len(want) != sha512.Size {
return fmt.Errorf("invalid SHA512 for %q", artifact.URL)
}
path := filepath.Join(directory, artifact.URL)
stat, err := os.Lstat(path)
if err != nil || !stat.Mode().IsRegular() {
return fmt.Errorf("artifact must exist as a regular non-symlink file: %q", artifact.URL)
}
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
hash := sha512.New()
size, err := io.Copy(hash, file)
if err != nil {
return err
}
if size <= 0 || (artifact.Size != nil && size != *artifact.Size) || !bytes.Equal(want, hash.Sum(nil)) {
return fmt.Errorf("distributed artifact does not match updater size/SHA512: %q", artifact.URL)
}
return nil
}
Loading
Loading