mirror of
https://github.com/Sea-Haven-Industries/.github.git
synced 2026-09-30 10:33:12 +00:00
feat(workflows): add release and ci-mobile-ios reusable workflows
release.yaml is workflow_call-only. It normalises and validates a `version` input against MAJOR.MINOR.PATCH, skips every mutating step when the tag or a Release for it already exists, creates an annotated tag with `git tag -a` and publishes a GitHub Release with `gh release create --verify-tag`. Top-level permissions grant `contents: write` only; no id-token is requested. The previous-tag lookup and `--generate-notes` both work in a repo with no tags. ci-mobile-ios.yaml is workflow_call-only and pairs with cd-mobile-ios.yaml, reusing its node-version, ruby-version, working-directory, cache-dependency-path and fastlane-lane input names. Job `js` runs npm ci, typecheck, optional lint and optional tests on ubuntu-latest. Job `ios-build` runs pod install and `xcodebuild build` on macos-26 with CODE_SIGNING_ALLOWED=NO, CODE_SIGNING_REQUIRED=NO and CODE_SIGN_IDENTITY=""; it declares no secrets and performs no upload. Job `ci` aggregates both via `needs` so a caller job keyed `ci` reports `ci / ci`. All three jobs carry job-level concurrency with cancel-in-progress: true. Top-level permissions are `contents: read`. The Fastlane branch carries a per-line `# shellcheck disable=SC2086` because fastlane requires the platform and lane as two argv entries.
This commit is contained in:
parent
7a8243248b
commit
9389e51c10
2 changed files with 580 additions and 0 deletions
301
.github/workflows/ci-mobile-ios.yaml
vendored
Normal file
301
.github/workflows/ci-mobile-ios.yaml
vendored
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
name: CI — Mobile iOS
|
||||
|
||||
# Reusable CI counterpart to cd-mobile-ios.yaml. Verifies a React Native iOS
|
||||
# app before merge: dependency install, typecheck, optional lint, optional unit
|
||||
# tests, and a compile that is neither signed nor uploaded.
|
||||
#
|
||||
# Emits the single `ci / ci` status context required by the org branch-
|
||||
# protection rulesets. As in ci-python-app.yaml, `ci` is an aggregator job
|
||||
# gated on `needs`, so a caller job keyed `ci` reports `ci / ci` and that one
|
||||
# context is red whenever any job below it failed.
|
||||
#
|
||||
# Input names mirror cd-mobile-ios.yaml wherever the same concept exists:
|
||||
# node-version, ruby-version, working-directory, cache-dependency-path,
|
||||
# fastlane-lane.
|
||||
#
|
||||
# Runner split. The JS checks run on ubuntu-latest; only the native compile
|
||||
# runs on macOS, because only that step needs Xcode and CocoaPods. The macOS
|
||||
# runner is billed at a multiple of the Linux rate, so putting `npm ci` +
|
||||
# typecheck + tests on Linux keeps the expensive runner to the one job that
|
||||
# genuinely requires it. `macos-26` matches cd-mobile-ios.yaml's runner, so CI
|
||||
# compiles on the same Xcode image the deploy builds on.
|
||||
#
|
||||
# The compile is a `xcodebuild build`, not `archive` + `export`: it passes
|
||||
# CODE_SIGNING_ALLOWED=NO / CODE_SIGNING_REQUIRED=NO / CODE_SIGN_IDENTITY="",
|
||||
# and there is no App Store Connect or fastlane match step anywhere in this
|
||||
# file. It therefore needs no signing certificates and no secrets, which is why
|
||||
# `workflow_call` here declares none.
|
||||
#
|
||||
# run-lint and run-tests default to FALSE. The only current caller of
|
||||
# cd-mobile-ios (proposal-system, working-directory `mobile`) declares exactly
|
||||
# five npm scripts — start, ios, android, typecheck, postinstall — so a lint or
|
||||
# test script cannot be assumed to exist. Turn them on per repo once the
|
||||
# scripts are there.
|
||||
#
|
||||
# Caller example:
|
||||
# jobs:
|
||||
# ci:
|
||||
# uses: Sea-Haven-Industries/.github/.github/workflows/ci-mobile-ios.yaml@<full-commit-sha> # main
|
||||
# with:
|
||||
# working-directory: mobile
|
||||
# cache-dependency-path: mobile/package-lock.json
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
node-version:
|
||||
description: "Node.js version to use"
|
||||
type: string
|
||||
default: "24"
|
||||
ruby-version:
|
||||
description: "Ruby version for CocoaPods / Fastlane"
|
||||
type: string
|
||||
default: "3.3"
|
||||
working-directory:
|
||||
description: "Directory containing the mobile project"
|
||||
type: string
|
||||
default: "."
|
||||
cache-dependency-path:
|
||||
description: "Path to package-lock.json for npm cache"
|
||||
type: string
|
||||
default: "package-lock.json"
|
||||
run-typecheck:
|
||||
description: "Run the typecheck npm script"
|
||||
type: boolean
|
||||
default: true
|
||||
typecheck-script:
|
||||
description: "npm script name for the TypeScript check"
|
||||
type: string
|
||||
default: "typecheck"
|
||||
run-lint:
|
||||
description: "Run the lint npm script. The script must exist in package.json."
|
||||
type: boolean
|
||||
default: false
|
||||
lint-script:
|
||||
description: "npm script name for the linter"
|
||||
type: string
|
||||
default: "lint"
|
||||
run-tests:
|
||||
description: "Run the test npm script. The script must exist in package.json."
|
||||
type: boolean
|
||||
default: false
|
||||
test-script:
|
||||
description: "npm script name for the unit tests"
|
||||
type: string
|
||||
default: "test"
|
||||
run-ios-build:
|
||||
description: "Compile the iOS app without signing it"
|
||||
type: boolean
|
||||
default: true
|
||||
fastlane-lane:
|
||||
description: "Fastlane lane to run instead of xcodebuild. Empty means call xcodebuild directly. The lane must not sign or upload."
|
||||
type: string
|
||||
default: ""
|
||||
xcode-workspace:
|
||||
description: "Path to the .xcworkspace, relative to working-directory. Empty means auto-detect the one under ios/."
|
||||
type: string
|
||||
default: ""
|
||||
xcode-scheme:
|
||||
description: "Xcode scheme to build. Empty means the workspace file name without its extension."
|
||||
type: string
|
||||
default: ""
|
||||
xcode-configuration:
|
||||
description: "Xcode build configuration"
|
||||
type: string
|
||||
default: "Debug"
|
||||
js-timeout-minutes:
|
||||
description: "Timeout in minutes for the JavaScript checks job"
|
||||
type: number
|
||||
default: 15
|
||||
ios-timeout-minutes:
|
||||
description: "Timeout in minutes for the iOS compile job"
|
||||
type: number
|
||||
default: 45
|
||||
|
||||
# Read-only: this workflow builds and tests, it publishes nothing and assumes
|
||||
# no cloud role. No `id-token` — nothing here mints an OIDC token.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
js:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: ${{ inputs.js-timeout-minutes }}
|
||||
# cancel-in-progress is TRUE: superseding a push should abandon the older
|
||||
# CI run, which produces no external side effects. `github.job` is in the
|
||||
# key so the three jobs here do not serialise against each other.
|
||||
concurrency:
|
||||
group: ci-mobile-ios-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-${{ github.job }}
|
||||
cancel-in-progress: true
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
cache: npm
|
||||
cache-dependency-path: ${{ inputs.cache-dependency-path }}
|
||||
|
||||
- name: Install JS dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Verify the requested npm scripts exist
|
||||
env:
|
||||
RUN_TYPECHECK: ${{ inputs.run-typecheck }}
|
||||
TYPECHECK_SCRIPT: ${{ inputs.typecheck-script }}
|
||||
RUN_LINT: ${{ inputs.run-lint }}
|
||||
LINT_SCRIPT: ${{ inputs.lint-script }}
|
||||
RUN_TESTS: ${{ inputs.run-tests }}
|
||||
TEST_SCRIPT: ${{ inputs.test-script }}
|
||||
run: |
|
||||
node <<'NODE'
|
||||
const { readFileSync } = require("node:fs");
|
||||
const pkg = JSON.parse(readFileSync("package.json", "utf8"));
|
||||
const wanted = [
|
||||
[process.env.RUN_TYPECHECK, process.env.TYPECHECK_SCRIPT],
|
||||
[process.env.RUN_LINT, process.env.LINT_SCRIPT],
|
||||
[process.env.RUN_TESTS, process.env.TEST_SCRIPT],
|
||||
];
|
||||
const missing = wanted
|
||||
.filter(([enabled, name]) => enabled === "true" && name)
|
||||
.map(([, name]) => name)
|
||||
.filter((name) => !pkg.scripts?.[name]);
|
||||
|
||||
if (missing.length > 0) {
|
||||
console.error(`Enabled but missing from package.json scripts: ${missing.join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("All enabled npm scripts are present.");
|
||||
NODE
|
||||
|
||||
- name: Typecheck
|
||||
if: ${{ inputs.run-typecheck }}
|
||||
env:
|
||||
TYPECHECK_SCRIPT: ${{ inputs.typecheck-script }}
|
||||
run: npm run "${TYPECHECK_SCRIPT}"
|
||||
|
||||
- name: Lint
|
||||
if: ${{ inputs.run-lint }}
|
||||
env:
|
||||
LINT_SCRIPT: ${{ inputs.lint-script }}
|
||||
run: npm run "${LINT_SCRIPT}"
|
||||
|
||||
- name: Unit tests
|
||||
if: ${{ inputs.run-tests }}
|
||||
env:
|
||||
TEST_SCRIPT: ${{ inputs.test-script }}
|
||||
run: npm run "${TEST_SCRIPT}"
|
||||
|
||||
ios-build:
|
||||
if: ${{ inputs.run-ios-build }}
|
||||
runs-on: macos-26
|
||||
timeout-minutes: ${{ inputs.ios-timeout-minutes }}
|
||||
concurrency:
|
||||
group: ci-mobile-ios-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-${{ github.job }}
|
||||
cancel-in-progress: true
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
cache: npm
|
||||
cache-dependency-path: ${{ inputs.cache-dependency-path }}
|
||||
|
||||
- uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1
|
||||
with:
|
||||
ruby-version: ${{ inputs.ruby-version }}
|
||||
bundler-cache: true
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
|
||||
- name: Install JS dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install CocoaPods
|
||||
run: bundle exec pod install --project-directory=ios
|
||||
|
||||
- name: Point the Xcode build phase at the runner's node
|
||||
# React Native's "Bundle React Native code and images" build phase
|
||||
# resolves node through .xcode.env.local. cd-mobile-ios.yaml gets this
|
||||
# from the repo's Fastfile; the xcodebuild path below has no Fastfile
|
||||
# step, so write it here.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node_path="$(command -v node)"
|
||||
printf 'export NODE_BINARY=%s\n' "${node_path}" > ios/.xcode.env.local
|
||||
echo "NODE_BINARY set to ${node_path}"
|
||||
|
||||
- name: Build without signing
|
||||
if: ${{ inputs.fastlane-lane == '' }}
|
||||
env:
|
||||
XCODE_WORKSPACE: ${{ inputs.xcode-workspace }}
|
||||
XCODE_SCHEME: ${{ inputs.xcode-scheme }}
|
||||
XCODE_CONFIGURATION: ${{ inputs.xcode-configuration }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
workspace="${XCODE_WORKSPACE}"
|
||||
if [ -z "${workspace}" ]; then
|
||||
workspace="$(find ios -maxdepth 1 -name '*.xcworkspace' | head -n 1)"
|
||||
fi
|
||||
|
||||
if [ -z "${workspace}" ] || [ ! -d "${workspace}" ]; then
|
||||
echo "::error::No .xcworkspace found. Set xcode-workspace explicitly."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
scheme="${XCODE_SCHEME}"
|
||||
if [ -z "${scheme}" ]; then
|
||||
scheme="$(basename "${workspace}" .xcworkspace)"
|
||||
fi
|
||||
|
||||
echo "Building workspace ${workspace}, scheme ${scheme}, configuration ${XCODE_CONFIGURATION}."
|
||||
|
||||
# `build`, not `archive`: no .ipa is produced and nothing is exported.
|
||||
# The three CODE_SIGN* settings turn signing off entirely, so this
|
||||
# needs no certificates and cannot upload anything.
|
||||
xcodebuild build \
|
||||
-workspace "${workspace}" \
|
||||
-scheme "${scheme}" \
|
||||
-configuration "${XCODE_CONFIGURATION}" \
|
||||
-destination 'generic/platform=iOS' \
|
||||
-derivedDataPath "${RUNNER_TEMP}/DerivedData" \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
CODE_SIGN_IDENTITY=""
|
||||
|
||||
- name: Build via Fastlane
|
||||
if: ${{ inputs.fastlane-lane != '' }}
|
||||
env:
|
||||
FASTLANE_LANE: ${{ inputs.fastlane-lane }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Deliberately word-split: `fastlane-lane` carries a platform and a
|
||||
# lane ("ios build") that fastlane expects as two separate argv
|
||||
# entries, exactly as cd-mobile-ios.yaml passes it. Quoting would
|
||||
# send one argument and fastlane would not find the lane.
|
||||
# shellcheck disable=SC2086
|
||||
bundle exec fastlane ${FASTLANE_LANE}
|
||||
|
||||
ci:
|
||||
# Aggregator — keyed `ci` so a caller job keyed `ci` reports `ci / ci`.
|
||||
needs: [js, ios-build]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: ci-mobile-ios-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-${{ github.job }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Require all jobs to have succeeded
|
||||
run: |
|
||||
if [ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "true" ]; then
|
||||
echo "A required CI job failed or was cancelled."
|
||||
exit 1
|
||||
fi
|
||||
echo "All CI jobs passed."
|
||||
279
.github/workflows/release.yaml
vendored
Normal file
279
.github/workflows/release.yaml
vendored
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
name: Release — Tag and GitHub Release
|
||||
|
||||
# Reusable release workflow: creates an annotated git tag at a commit and
|
||||
# publishes a GitHub Release pointing at it.
|
||||
#
|
||||
# Version derivation is an INPUT, not read from a manifest and not computed.
|
||||
# That follows what the org's repos actually contain:
|
||||
# - 3 of 23 non-archived repos carry any tag at all; every existing tag is
|
||||
# `vMAJOR.MINOR.PATCH`, which is why `tag-prefix` defaults to "v".
|
||||
# - `package.json` "version" is not maintained as a release marker where it
|
||||
# exists: seahaven-door-unlock-api sits at "1.0.0" while its tags reach
|
||||
# v3.0.0, and payments-dashboard sits at "1.0.0" with no tags at all.
|
||||
# - No repo has a VERSION file, and the repos that tag are Python/SAM,
|
||||
# TypeScript/CDK and Python-desktop, so there is no one manifest to read.
|
||||
# - The single repo that derives a version from a file
|
||||
# (afterhours-shift-manager, from CHANGELOG.md) does it with two
|
||||
# repo-local parser scripts under `scripts/`, which a reusable workflow
|
||||
# cannot assume exist.
|
||||
# An explicit input is therefore the only derivation that works unchanged for
|
||||
# every repo here. A repo that does maintain a machine-readable version can
|
||||
# still pass it: `version: ${{ needs.x.outputs.version }}`.
|
||||
#
|
||||
# Re-running on a version that is already released is a no-op, not a failure:
|
||||
# the tag and the Release are both checked first, and either one being present
|
||||
# skips every mutating step. This mirrors afterhours-shift-manager's release
|
||||
# job, which likewise no-ops when the Release already exists.
|
||||
#
|
||||
# Safe in a repo with no releases yet: the "previous tag" lookup tolerates zero
|
||||
# tags, and `gh release create --generate-notes` falls back to the full commit
|
||||
# history when there is no earlier release to diff against.
|
||||
#
|
||||
# Caller example:
|
||||
# jobs:
|
||||
# release:
|
||||
# uses: Sea-Haven-Industries/.github/.github/workflows/release.yaml@<full-commit-sha> # main
|
||||
# with:
|
||||
# version: ${{ inputs.version }}
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to release, e.g. "1.4.0". A leading "v" is accepted and stripped.'
|
||||
type: string
|
||||
required: true
|
||||
tag-prefix:
|
||||
description: "Prefix placed in front of the version to form the tag name"
|
||||
type: string
|
||||
default: "v"
|
||||
target:
|
||||
description: "Commit-ish to tag. Empty means the commit the workflow was triggered on."
|
||||
type: string
|
||||
default: ""
|
||||
notes-file:
|
||||
description: "Path to a file whose contents become the release notes. Empty means use generate-notes."
|
||||
type: string
|
||||
default: ""
|
||||
generate-notes:
|
||||
description: "Let GitHub generate release notes from commits when notes-file is empty"
|
||||
type: boolean
|
||||
default: true
|
||||
title:
|
||||
description: "Release title. Empty means use the tag name."
|
||||
type: string
|
||||
default: ""
|
||||
draft:
|
||||
description: "Publish the Release as a draft"
|
||||
type: boolean
|
||||
default: false
|
||||
prerelease:
|
||||
description: "Mark the Release as a prerelease"
|
||||
type: boolean
|
||||
default: false
|
||||
timeout-minutes:
|
||||
description: "Job timeout in minutes"
|
||||
type: number
|
||||
default: 10
|
||||
outputs:
|
||||
tag:
|
||||
description: "The tag name that was created, or that already existed"
|
||||
value: ${{ jobs.release.outputs.tag }}
|
||||
version:
|
||||
description: "The normalised version, without the tag prefix"
|
||||
value: ${{ jobs.release.outputs.version }}
|
||||
released:
|
||||
description: '"true" when this run created the tag and Release, "false" when it skipped'
|
||||
value: ${{ jobs.release.outputs.released }}
|
||||
url:
|
||||
description: "URL of the Release this run created. Empty when the run skipped."
|
||||
value: ${{ jobs.release.outputs.url }}
|
||||
|
||||
# Only `contents: write` — needed to push the tag and publish the Release.
|
||||
# Nothing here mints an OIDC token, so `id-token` is deliberately not granted.
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: ${{ inputs.timeout-minutes }}
|
||||
# Serialise per tag so two runs cannot race to create the same release.
|
||||
# version is required and tag-prefix always defaults, so the group is never
|
||||
# empty. cancel-in-progress is FALSE on purpose: unlike CI, aborting midway
|
||||
# can leave a pushed tag with no Release attached to it.
|
||||
concurrency:
|
||||
group: release-${{ github.repository }}-${{ inputs.tag-prefix }}${{ inputs.version }}
|
||||
cancel-in-progress: false
|
||||
outputs:
|
||||
tag: ${{ steps.resolve.outputs.tag }}
|
||||
version: ${{ steps.resolve.outputs.version }}
|
||||
released: ${{ steps.publish.outputs.released || 'false' }}
|
||||
url: ${{ steps.publish.outputs.url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
# Full history + tags: the existing-tag guard reads local refs.
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
ref: ${{ inputs.target }}
|
||||
|
||||
- name: Resolve and validate version
|
||||
id: resolve
|
||||
# Inputs are passed through env, never interpolated into the script
|
||||
# body, so a caller cannot inject shell into this step.
|
||||
env:
|
||||
RAW_VERSION: ${{ inputs.version }}
|
||||
TAG_PREFIX: ${{ inputs.tag-prefix }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
version="${RAW_VERSION#v}"
|
||||
|
||||
if ! printf '%s' "${version}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'; then
|
||||
echo "::error::version '${RAW_VERSION}' is not MAJOR.MINOR.PATCH with an optional -prerelease/+build suffix."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tag="${TAG_PREFIX}${version}"
|
||||
|
||||
{
|
||||
echo "version=${version}"
|
||||
echo "tag=${tag}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
|
||||
echo "Resolved ${RAW_VERSION} to tag ${tag}."
|
||||
|
||||
- name: Check whether the tag or Release already exists
|
||||
id: guard
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ steps.resolve.outputs.tag }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
skip=false
|
||||
reason=""
|
||||
|
||||
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
|
||||
skip=true
|
||||
reason="tag ${TAG} already exists locally"
|
||||
elif [ -n "$(git ls-remote --tags origin "refs/tags/${TAG}")" ]; then
|
||||
skip=true
|
||||
reason="tag ${TAG} already exists on the remote"
|
||||
elif gh release view "${TAG}" --repo "${REPO}" >/dev/null 2>&1; then
|
||||
skip=true
|
||||
reason="a Release for ${TAG} is already published"
|
||||
fi
|
||||
|
||||
echo "skip=${skip}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
if [ "${skip}" = "true" ]; then
|
||||
echo "::notice::Skipping — ${reason}."
|
||||
else
|
||||
echo "No existing tag or Release for ${TAG}."
|
||||
fi
|
||||
|
||||
- name: Report the previous tag
|
||||
if: ${{ steps.guard.outputs.skip == 'false' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
previous="$(git tag -l --sort=-v:refname | head -n 1)"
|
||||
|
||||
if [ -z "${previous}" ]; then
|
||||
echo "No previous tag in this repository — this is the first release."
|
||||
else
|
||||
echo "Previous tag: ${previous}"
|
||||
fi
|
||||
|
||||
- name: Resolve release notes
|
||||
id: notes
|
||||
if: ${{ steps.guard.outputs.skip == 'false' }}
|
||||
env:
|
||||
NOTES_FILE: ${{ inputs.notes-file }}
|
||||
GENERATE_NOTES: ${{ inputs.generate-notes }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ -n "${NOTES_FILE}" ]; then
|
||||
if [ ! -f "${NOTES_FILE}" ]; then
|
||||
echo "::error::notes-file '${NOTES_FILE}' does not exist."
|
||||
exit 1
|
||||
fi
|
||||
echo "mode=file" >> "${GITHUB_OUTPUT}"
|
||||
echo "Using release notes from ${NOTES_FILE}."
|
||||
elif [ "${GENERATE_NOTES}" = "true" ]; then
|
||||
echo "mode=generate" >> "${GITHUB_OUTPUT}"
|
||||
echo "Release notes will be generated from commit history."
|
||||
else
|
||||
echo "mode=empty" >> "${GITHUB_OUTPUT}"
|
||||
echo "Publishing with empty release notes."
|
||||
fi
|
||||
|
||||
- name: Create and push the annotated tag
|
||||
if: ${{ steps.guard.outputs.skip == 'false' }}
|
||||
env:
|
||||
TAG: ${{ steps.resolve.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# -a makes this an annotated tag: it is a real tag object carrying a
|
||||
# tagger, a date and a message, unlike a lightweight ref.
|
||||
git -c user.name='github-actions[bot]' \
|
||||
-c user.email='41898282+github-actions[bot]@users.noreply.github.com' \
|
||||
tag -a "${TAG}" -m "${TAG}" "${GITHUB_SHA}"
|
||||
|
||||
git push origin "refs/tags/${TAG}"
|
||||
|
||||
echo "Pushed annotated tag ${TAG} at ${GITHUB_SHA}."
|
||||
|
||||
- name: Publish the GitHub Release
|
||||
id: publish
|
||||
if: ${{ steps.guard.outputs.skip == 'false' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
TAG: ${{ steps.resolve.outputs.tag }}
|
||||
TITLE: ${{ inputs.title }}
|
||||
NOTES_MODE: ${{ steps.notes.outputs.mode }}
|
||||
NOTES_FILE: ${{ inputs.notes-file }}
|
||||
DRAFT: ${{ inputs.draft }}
|
||||
PRERELEASE: ${{ inputs.prerelease }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
args=(release create "${TAG}" --repo "${REPO}" --verify-tag)
|
||||
args+=(--title "${TITLE:-${TAG}}")
|
||||
|
||||
case "${NOTES_MODE}" in
|
||||
file) args+=(--notes-file "${NOTES_FILE}") ;;
|
||||
generate) args+=(--generate-notes) ;;
|
||||
*) args+=(--notes "") ;;
|
||||
esac
|
||||
|
||||
if [ "${DRAFT}" = "true" ]; then
|
||||
args+=(--draft)
|
||||
fi
|
||||
|
||||
if [ "${PRERELEASE}" = "true" ]; then
|
||||
args+=(--prerelease)
|
||||
fi
|
||||
|
||||
gh "${args[@]}"
|
||||
|
||||
url="$(gh release view "${TAG}" --repo "${REPO}" --json url --jq .url)"
|
||||
|
||||
{
|
||||
echo "released=true"
|
||||
echo "url=${url}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
|
||||
echo "Published ${url}"
|
||||
|
||||
- name: Report a skipped run
|
||||
if: ${{ steps.guard.outputs.skip == 'true' }}
|
||||
env:
|
||||
TAG: ${{ steps.resolve.outputs.tag }}
|
||||
run: echo "${TAG} was already released. Nothing to do."
|
||||
Loading…
Add table
Reference in a new issue