1# The release recipe (issue #5 §7, M7).2#3# Two things ship from this repository, and this workflow builds both.4#5# atlas the CLI: crawl, tiles, translate, compose, enrich, measure,6# workbench, serve, dev. Pure Go, CGO_ENABLED=0, cross-compiled for7# five targets from one runner.8# Atlas the desktop application: the hypermedia app in a webview (§3.4).9# It links a native webview, so it is built and packaged on the10# platform it runs on. Each package registers .atlas with the OS.11#12# It replaces ci.yml, which built and released the pre-rewrite desktop shell on13# `v*` tags until the old tree was archived at close-out. `v*` answers here14# now. `atlas-v*` is still accepted -- it is the tag the CLI shipped under15# while the desktop host was a TODO in this file -- and means the same thing.16#17# The recipe carried from ci.yml, unchanged where it was already right:18#19# - version from the tag, `dev-<short sha>` off a tag;20# - `-trimpath` and `-ldflags "-s -w"`, so a build is a function of its21# inputs and carries no local paths;22# - CLI archives remain .tar.gz/.zip; desktop delivery is an application23# bundle on macOS, a MIME-registering .deb on Linux, and an association-24# registering Inno Setup installer on Windows;25# - a release job gated on the tag, `--generate-notes --verify-tag`, and a26# prerelease when the version carries a hyphen.27#28# The one addition is the seam. `make static` builds render/ and installs the29# bundle where the shell embeds it, so a desktop build runs it first; the CLI30# needs none of that, which is the deletability principle showing up in the31# release recipe.3233name: Release3435on:36 push:37 tags: ["v*", "atlas-v*"]38 workflow_dispatch:39 inputs:40 version:41 description: "version label for a dispatched build, e.g. v0.1.0"42 required: false43 type: string4445permissions:46 contents: read4748concurrency:49 group: release-${{ github.ref }}50 cancel-in-progress: false5152env:53 GOFLAGS: -trimpath -buildvcs=false5455jobs:56 verify:57 name: release acceptance58 runs-on: ubuntu-24.0459 steps:60 - uses: actions/checkout@v561 - uses: actions/setup-go@v562 with:63 go-version-file: go.mod64 - uses: actions/setup-node@v465 with:66 node-version: "24"67 cache: npm68 cache-dependency-path: package-lock.json69 - name: Install test dependencies70 run: |71 npm ci --ignore-scripts72 npx playwright install --with-deps chromium73 - name: Required correctness gates74 run: |75 make test76 make test-e2e7778 cli:79 name: atlas ${{ matrix.goos }}/${{ matrix.goarch }}80 runs-on: ubuntu-24.0481 env:82 CGO_ENABLED: "0"83 strategy:84 fail-fast: false85 matrix:86 include:87 - { goos: linux, goarch: amd64 }88 - { goos: linux, goarch: arm64 }89 - { goos: darwin, goarch: arm64 }90 - { goos: darwin, goarch: amd64 }91 - { goos: windows, goarch: amd64 }92 steps:93 - uses: actions/checkout@v594 - uses: actions/setup-go@v595 with:96 go-version-file: go.mod9798 - name: Build99 env:100 GOOS: ${{ matrix.goos }}101 GOARCH: ${{ matrix.goarch }}102 run: |103 VERSION="${GITHUB_REF_NAME#atlas-}"104 if [[ "${GITHUB_REF}" != refs/tags/* ]]; then105 VERSION="${{ inputs.version }}"106 [[ -n "${VERSION}" ]] || VERSION="dev-$(git rev-parse --short HEAD)"107 fi108 # "atlas-cli-", not "atlas-": GitHub release asset names are109 # case-insensitively unique, so a CLI archive spelled "atlas-…"110 # collides with the desktop's "Atlas-…" wherever the two share a111 # platform and an extension — which is exactly how v0.1.0's release112 # job died half-published. The prefix also reads better to a person113 # deciding which file they want.114 NAME="atlas-cli-${VERSION}-${GOOS}-${GOARCH}"115 BINARY="atlas"116 if [[ "${GOOS}" == "windows" ]]; then BINARY="atlas.exe"; fi117 mkdir -p "dist/${NAME}"118 go build -ldflags "-s -w -X main.version=${VERSION}" -o "dist/${NAME}/${BINARY}" ./cmd/atlas119 cp README.md LICENSE "dist/${NAME}/"120 cd dist121 if [[ "${GOOS}" == "windows" ]]; then122 zip -qr "${NAME}.zip" "${NAME}"123 else124 tar -czf "${NAME}.tar.gz" "${NAME}"125 fi126127 - uses: actions/upload-artifact@v4128 with:129 name: atlas-${{ matrix.goos }}-${{ matrix.goarch }}130 path: |131 dist/*.tar.gz132 dist/*.zip133 if-no-files-found: error134135 desktop:136 name: Atlas ${{ matrix.target }}137 runs-on: ${{ matrix.os }}138 strategy:139 fail-fast: false140 matrix:141 include:142 - target: linux-amd64143 os: ubuntu-24.04144 tags: "desktop,production,webkit2_41"145 cgo: "1"146 - target: windows-amd64147 os: windows-2025148 tags: "desktop,production"149 cgo: "0"150 # -H windowsgui is what stops a console window opening behind the151 # application.152 ldflags: "-H windowsgui"153 - target: darwin-arm64154 os: macos-15155 tags: "desktop,production"156 cgo: "1"157 steps:158 - uses: actions/checkout@v5159 - uses: actions/setup-go@v5160 with:161 go-version-file: go.mod162 - uses: actions/setup-node@v4163 with:164 node-version: "24"165 cache: npm166 cache-dependency-path: package-lock.json167168 # The seam, built and installed where main.go embeds it. Without this169 # step the application still builds and still runs -- it would ship170 # without a viewport, which is the deletability principle rather than an171 # accident -- so the step asserts its own output, to be noticed if it172 # ever silently stops producing one.173 - name: Build the seam into the shell's static tree174 shell: bash175 run: |176 npm ci --ignore-scripts177 npm run --silent seam-build178 mkdir -p dist/static static179 cp render/dist/app.js dist/static/app.js180 cp render/dist/app.js static/app.js181 test -s static/app.js182183 - name: Install GTK and WebKitGTK headers184 if: matrix.target == 'linux-amd64'185 run: |186 sudo apt-get update187 sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev188189 - name: Resolve version190 id: metadata191 shell: bash192 run: |193 VERSION="${GITHUB_REF_NAME#atlas-}"194 if [[ "${GITHUB_REF}" != refs/tags/* ]]; then195 VERSION="${{ inputs.version }}"196 [[ -n "${VERSION}" ]] || VERSION="dev-$(git rev-parse --short HEAD)"197 fi198 echo "version=${VERSION}" >> "$GITHUB_OUTPUT"199200 - name: Build native desktop binary201 shell: bash202 env:203 CGO_ENABLED: ${{ matrix.cgo }}204 run: |205 BINARY="Atlas"206 if [[ "${{ matrix.target }}" == windows-* ]]; then BINARY="Atlas.exe"; fi207 go build -tags "${{ matrix.tags }}" \208 -ldflags "-s -w -X main.version=${{ steps.metadata.outputs.version }} ${{ matrix.ldflags }}" -o "${BINARY}" .209210 - name: Sign Windows application when credentials are configured211 if: matrix.target == 'windows-amd64'212 shell: pwsh213 env:214 WINDOWS_CERTIFICATE_BASE64: ${{ secrets.WINDOWS_CERTIFICATE_BASE64 }}215 WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}216 run: ./scripts/release/sign-windows.ps1 -Path Atlas.exe217218 - name: Package macOS application219 if: matrix.target == 'darwin-arm64'220 shell: bash221 env:222 MACOS_CERTIFICATE_P12_BASE64: ${{ secrets.MACOS_CERTIFICATE_P12_BASE64 }}223 MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}224 MACOS_SIGNING_IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }}225 MACOS_NOTARY_KEY_BASE64: ${{ secrets.MACOS_NOTARY_KEY_BASE64 }}226 MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }}227 MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }}228 run: ./scripts/release/package-desktop.sh darwin-arm64 "${{ steps.metadata.outputs.version }}" Atlas dist229230 - name: Package Linux application231 if: matrix.target == 'linux-amd64'232 shell: bash233 run: ./scripts/release/package-desktop.sh linux-amd64 "${{ steps.metadata.outputs.version }}" Atlas dist234235 - name: Smoke macOS package and native file intake236 if: matrix.target == 'darwin-arm64'237 shell: bash238 run: ./scripts/release/smoke-macos.sh "dist/Atlas-${{ steps.metadata.outputs.version }}-darwin-arm64.zip" "${{ steps.metadata.outputs.version }}" "$GITHUB_WORKSPACE"239240 - name: Smoke Linux install, MIME registration, and native file intake241 if: matrix.target == 'linux-amd64'242 shell: bash243 run: ./scripts/release/smoke-linux.sh "dist/Atlas-${{ steps.metadata.outputs.version }}-linux-amd64.deb" "${{ steps.metadata.outputs.version }}" "$GITHUB_WORKSPACE"244245 - name: Package Windows installer246 if: matrix.target == 'windows-amd64'247 shell: pwsh248 run: ./scripts/release/package-windows.ps1 -Version "${{ steps.metadata.outputs.version }}" -Binary "${{ github.workspace }}\Atlas.exe" -Dist "${{ github.workspace }}\dist"249250 - name: Sign Windows installer when credentials are configured251 if: matrix.target == 'windows-amd64'252 shell: pwsh253 env:254 WINDOWS_CERTIFICATE_BASE64: ${{ secrets.WINDOWS_CERTIFICATE_BASE64 }}255 WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}256 run: ./scripts/release/sign-windows.ps1 -Path "dist/Atlas-${{ steps.metadata.outputs.version }}-windows-amd64-setup.exe"257258 - name: Smoke Windows install and .atlas association259 if: matrix.target == 'windows-amd64'260 shell: pwsh261 run: ./scripts/release/smoke-windows.ps1 -Installer "dist/Atlas-${{ steps.metadata.outputs.version }}-windows-amd64-setup.exe" -Version "${{ steps.metadata.outputs.version }}" -RepoRoot "${{ github.workspace }}"262263 - uses: actions/upload-artifact@v4264 with:265 name: Atlas-${{ matrix.target }}266 path: |267 dist/*.deb268 dist/*.exe269 dist/*.tar.gz270 dist/*.zip271 if-no-files-found: error272273 release:274 if: startsWith(github.ref, 'refs/tags/')275 needs: [verify, cli, desktop]276 runs-on: ubuntu-24.04277 permissions:278 contents: write279 steps:280 - uses: actions/checkout@v5281 - uses: actions/download-artifact@v4282 with:283 path: dist284 merge-multiple: true285 - name: Verify the exact asset set and smoke the packaged CLI286 run: |287 VERSION="${GITHUB_REF_NAME#atlas-}"288 scripts/release/verify-assets.sh dist "$VERSION"289 scripts/release/package-smoke.sh dist "$VERSION" "$GITHUB_WORKSPACE"290291 - name: Publish a complete release transaction292 env:293 GH_TOKEN: ${{ github.token }}294 run: |295 # A hyphen in the version itself -- v0.2.0-rc1 -- is a prerelease.296 VERSION="${GITHUB_REF_NAME#atlas-}"297 PRERELEASE=""298 if [[ "${VERSION#v}" == *-* ]]; then PRERELEASE="--prerelease"; fi299 gh release create "${GITHUB_REF_NAME}" \300 --title "Atlas ${VERSION}" \301 --generate-notes --verify-tag --draft ${PRERELEASE}302 gh release upload "${GITHUB_REF_NAME}" dist/*303304 mapfile -t LOCAL < <(find dist -maxdepth 1 -type f -printf '%f\n' | sort)305 mapfile -t REMOTE < <(gh release view "${GITHUB_REF_NAME}" --json assets --jq '.assets[].name' | sort)306 [[ "${#LOCAL[@]}" -eq 9 && "${#REMOTE[@]}" -eq 9 ]]307 diff -u <(printf '%s\n' "${LOCAL[@]}") <(printf '%s\n' "${REMOTE[@]}")308309 while read -r CHECKSUM NAME; do310 REMOTE_DIGEST="$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${GITHUB_REF_NAME}" \311 --jq ".assets[] | select(.name == \"${NAME}\") | .digest")"312 [[ "${REMOTE_DIGEST}" == "sha256:${CHECKSUM}" ]] || {313 printf 'remote digest mismatch for %s: wanted sha256:%s, got %s\n' \314 "${NAME}" "${CHECKSUM}" "${REMOTE_DIGEST}" >&2315 exit 1316 }317 done < dist/SHA256SUMS318 CHECKSUM="$(sha256sum dist/SHA256SUMS | cut -d ' ' -f 1)"319 REMOTE_DIGEST="$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${GITHUB_REF_NAME}" \320 --jq '.assets[] | select(.name == "SHA256SUMS") | .digest')"321 [[ "${REMOTE_DIGEST}" == "sha256:${CHECKSUM}" ]]322 gh release edit "${GITHUB_REF_NAME}" --draft=false