mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
Fix/release untagged guard (#399)
* fix(release): prevent untagged fallback from tauri publish Set explicit github-script output for release_id and validate it before build jobs. This prevents tauri-action from creating untagged releases when release id wiring breaks and keeps assets attached to the intended app-v tag release. * fix(release): add source-only promote mode and harden release binding Add a source_only dispatch option to promote workflows and propagate it into channel publish so maintainers can skip platform builds and verify-nix when needed. Also validate release/tag/commit binding to prevent untagged fallback and ensure source archives stay aligned with the intended app-v tag.
This commit is contained in:
@@ -1,8 +1,5 @@
|
||||
name: Next Channel
|
||||
|
||||
# Pushes made with the default GITHUB_TOKEN from another workflow (e.g. Promote main to next)
|
||||
# do not trigger push workflows — GitHub blocks recursive runs. Use workflow_run below.
|
||||
#
|
||||
# workflow_dispatch: set "Use workflow from branch" to `main` so reusable-channel-publish.yml
|
||||
# is the latest (tag guards, etc.). The tree built for artifacts is always ref `next` below.
|
||||
on:
|
||||
@@ -14,13 +11,15 @@ on:
|
||||
- "flake.lock"
|
||||
- "nix/**"
|
||||
workflow_dispatch:
|
||||
workflow_run:
|
||||
workflows: ["Promote main to next"]
|
||||
types: [completed]
|
||||
inputs:
|
||||
source_only:
|
||||
description: "Create/refresh tagged release only (skip platform builds and verify-nix)"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
publish-next:
|
||||
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
|
||||
uses: ./.github/workflows/reusable-channel-publish.yml
|
||||
with:
|
||||
channel: next
|
||||
@@ -29,5 +28,6 @@ jobs:
|
||||
target_branch: next
|
||||
prerelease: true
|
||||
draft_release: true
|
||||
verify_nix: true
|
||||
verify_nix: ${{ !(github.event_name == 'workflow_dispatch' && github.event.inputs.source_only == 'true') }}
|
||||
build_platform_artifacts: ${{ !(github.event_name == 'workflow_dispatch' && github.event.inputs.source_only == 'true') }}
|
||||
secrets: inherit
|
||||
|
||||
@@ -2,6 +2,12 @@ name: Promote main to next
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
source_only:
|
||||
description: "Create/refresh tagged release only (skip platform builds and verify-nix)"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
validate-main:
|
||||
@@ -15,6 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
@@ -64,3 +71,20 @@ jobs:
|
||||
git commit -m "chore(release): bump next channel to ${NEW_VERSION}"
|
||||
- name: push next
|
||||
run: git push --force-with-lease origin HEAD:next
|
||||
- name: dispatch Next Channel workflow
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
SOURCE_ONLY: ${{ inputs.source_only && 'true' || 'false' }}
|
||||
with:
|
||||
script: |
|
||||
const sourceOnly = process.env.SOURCE_ONLY === "true";
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: "next.yml",
|
||||
ref: "main",
|
||||
inputs: {
|
||||
source_only: sourceOnly ? "true" : "false",
|
||||
},
|
||||
});
|
||||
core.info(`Dispatched next.yml with source_only=${sourceOnly}`);
|
||||
|
||||
@@ -2,12 +2,19 @@ name: Promote next to release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
source_only:
|
||||
description: "Create/refresh tagged release only (skip platform builds and verify-nix)"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
promote:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
@@ -49,3 +56,20 @@ jobs:
|
||||
git commit -m "chore(release): finalize release version ${NEW_VERSION}"
|
||||
- name: push release
|
||||
run: git push --force-with-lease origin HEAD:release
|
||||
- name: dispatch Release Channel workflow
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
SOURCE_ONLY: ${{ inputs.source_only && 'true' || 'false' }}
|
||||
with:
|
||||
script: |
|
||||
const sourceOnly = process.env.SOURCE_ONLY === "true";
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: "release.yml",
|
||||
ref: "main",
|
||||
inputs: {
|
||||
source_only: sourceOnly ? "true" : "false",
|
||||
},
|
||||
});
|
||||
core.info(`Dispatched release.yml with source_only=${sourceOnly}`);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
name: Release Channel
|
||||
|
||||
# Same GITHUB_TOKEN push limitation as Next Channel (see next.yml).
|
||||
#
|
||||
# workflow_dispatch: use workflow from `main` for latest publish logic; checkout ref is always `release`.
|
||||
on:
|
||||
push:
|
||||
@@ -11,13 +9,15 @@ on:
|
||||
- "flake.lock"
|
||||
- "nix/**"
|
||||
workflow_dispatch:
|
||||
workflow_run:
|
||||
workflows: ["Promote next to release"]
|
||||
types: [completed]
|
||||
inputs:
|
||||
source_only:
|
||||
description: "Create/refresh tagged release only (skip platform builds and verify-nix)"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
publish-release:
|
||||
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
|
||||
uses: ./.github/workflows/reusable-channel-publish.yml
|
||||
with:
|
||||
channel: release
|
||||
@@ -25,5 +25,6 @@ jobs:
|
||||
target_branch: release
|
||||
prerelease: false
|
||||
draft_release: true
|
||||
verify_nix: true
|
||||
verify_nix: ${{ !(github.event_name == 'workflow_dispatch' && github.event.inputs.source_only == 'true') }}
|
||||
build_platform_artifacts: ${{ !(github.event_name == 'workflow_dispatch' && github.event.inputs.source_only == 'true') }}
|
||||
secrets: inherit
|
||||
|
||||
@@ -28,6 +28,11 @@ on:
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
build_platform_artifacts:
|
||||
description: "Build and upload macOS/Windows/Linux artifacts + latest.json"
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
@@ -35,7 +40,7 @@ jobs:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
release_id: ${{ steps.create-release.outputs.result }}
|
||||
release_id: ${{ steps.create_release.outputs.release_id }}
|
||||
package_version: ${{ steps.get-version.outputs.version }}
|
||||
release_tag: ${{ steps.tag.outputs.value }}
|
||||
source_commit_sha: ${{ steps.source-sha.outputs.value }}
|
||||
@@ -121,7 +126,7 @@ jobs:
|
||||
echo "$BODY" >> "$GITHUB_OUTPUT"
|
||||
echo "$EOF_MARKER" >> "$GITHUB_OUTPUT"
|
||||
- name: create or update release
|
||||
id: create-release
|
||||
id: create_release
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PACKAGE_VERSION: ${{ steps.get-version.outputs.version }}
|
||||
@@ -139,6 +144,7 @@ jobs:
|
||||
const version = process.env.PACKAGE_VERSION;
|
||||
const targetCommitish = process.env.RELEASE_COMMIT_SHA;
|
||||
const name = `Psysonic v${version}`;
|
||||
let releaseId = null;
|
||||
try {
|
||||
const { data } = await github.rest.repos.getReleaseByTag({
|
||||
owner: context.repo.owner,
|
||||
@@ -154,23 +160,90 @@ jobs:
|
||||
draft,
|
||||
prerelease,
|
||||
});
|
||||
return data.id;
|
||||
releaseId = data.id;
|
||||
core.info(`Updated existing release id=${releaseId} tag=${tag}`);
|
||||
} catch (e) {
|
||||
if (e.status !== 404) throw e;
|
||||
const { data } = await github.rest.repos.createRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
tag_name: tag,
|
||||
target_commitish: targetCommitish,
|
||||
name,
|
||||
body,
|
||||
draft,
|
||||
prerelease,
|
||||
});
|
||||
releaseId = data.id;
|
||||
core.info(`Created release id=${releaseId} tag=${tag} target_commitish=${targetCommitish}`);
|
||||
}
|
||||
const { data } = await github.rest.repos.createRelease({
|
||||
core.setOutput("release_id", String(releaseId));
|
||||
- name: validate release id output
|
||||
env:
|
||||
RELEASE_ID: ${{ steps.create_release.outputs.release_id }}
|
||||
TAG: ${{ steps.tag.outputs.value }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${RELEASE_ID:-}" ]; then
|
||||
echo "::error::create_release did not produce release_id for tag '$TAG'"
|
||||
exit 1
|
||||
fi
|
||||
if ! printf '%s' "$RELEASE_ID" | grep -qE '^[0-9]+$'; then
|
||||
echo "::error::release_id is not numeric: '$RELEASE_ID'"
|
||||
exit 1
|
||||
fi
|
||||
- name: verify release-tag binding
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
RELEASE_ID: ${{ steps.create_release.outputs.release_id }}
|
||||
EXPECTED_TAG: ${{ steps.tag.outputs.value }}
|
||||
EXPECTED_SHA: ${{ steps.source-sha.outputs.value }}
|
||||
with:
|
||||
script: |
|
||||
const releaseId = Number(process.env.RELEASE_ID);
|
||||
const expectedTag = process.env.EXPECTED_TAG;
|
||||
const expectedSha = process.env.EXPECTED_SHA;
|
||||
|
||||
const { data: release } = await github.rest.repos.getRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
tag_name: tag,
|
||||
target_commitish: targetCommitish,
|
||||
name,
|
||||
body,
|
||||
draft,
|
||||
prerelease,
|
||||
release_id: releaseId,
|
||||
});
|
||||
return data.id;
|
||||
|
||||
if (!release.tag_name || release.tag_name.startsWith("untagged-")) {
|
||||
core.setFailed(`release_id=${releaseId} is bound to untagged release '${release.tag_name ?? ""}'`);
|
||||
return;
|
||||
}
|
||||
if (release.tag_name !== expectedTag) {
|
||||
core.setFailed(`release_id=${releaseId} has tag '${release.tag_name}', expected '${expectedTag}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: ref } = await github.rest.git.getRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: `tags/${expectedTag}`,
|
||||
});
|
||||
|
||||
let resolvedSha = ref.object.sha;
|
||||
if (ref.object.type === "tag") {
|
||||
const { data: tagObj } = await github.rest.git.getTag({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
tag_sha: ref.object.sha,
|
||||
});
|
||||
resolvedSha = tagObj.object.sha;
|
||||
}
|
||||
|
||||
if (resolvedSha !== expectedSha) {
|
||||
core.setFailed(`tag '${expectedTag}' points to ${resolvedSha}, expected ${expectedSha}`);
|
||||
return;
|
||||
}
|
||||
|
||||
core.info(`Verified release_id=${releaseId}, tag=${expectedTag}, commit=${resolvedSha}`);
|
||||
|
||||
build-macos-windows:
|
||||
if: ${{ inputs.build_platform_artifacts }}
|
||||
needs: create-release
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -254,6 +327,7 @@ jobs:
|
||||
gh release upload "$RELEASE_TAG" "Psysonic_${ARCH}.app.tar.gz.sig" --clobber
|
||||
|
||||
generate-manifest:
|
||||
if: ${{ inputs.build_platform_artifacts }}
|
||||
needs: [create-release, build-macos-windows]
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
@@ -275,6 +349,7 @@ jobs:
|
||||
gh release upload "$RELEASE_TAG" latest.json --clobber
|
||||
|
||||
build-linux:
|
||||
if: ${{ inputs.build_platform_artifacts }}
|
||||
needs: create-release
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
Reference in New Issue
Block a user