Skip to content
Open
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
30 changes: 30 additions & 0 deletions .github/workflows/check-workflows.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
name: check-workflows

on:
push:
branches-ignore:
- 'development/**'
paths:
- '.github/workflows/**'
- 'tests/workflows/**'

jobs:
test-workflows:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install NodeJS
uses: actions/setup-node@v4
with:
node-version: '24'
cache: yarn
cache-dependency-path: tests/workflows/yarn.lock

- name: Install dependencies
run: yarn --cwd tests/workflows install --frozen-lockfile

- name: Run workflow tests
run: yarn --cwd tests/workflows test
71 changes: 71 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
name: release
run-name: release ${{ github.ref_name }}

on:
workflow_dispatch:

permissions:
contents: write

jobs:
release:
Comment thread
delthas marked this conversation as resolved.
runs-on: ubuntu-latest
steps:
- name: Reject disallowed branch
if: >-
${{ !startsWith(github.ref, 'refs/heads/development/')
&& !startsWith(github.ref, 'refs/heads/hotfix/') }}
env:
REF_NAME: ${{ github.ref_name }}
run: |
echo "::error::Releases must run from a development/* or" \
"hotfix/* branch (got $REF_NAME)"
exit 1

- name: Checkout
uses: actions/checkout@v4

- name: Fetch tags
run: git fetch --tags --force --quiet

- name: Read version from package.json
id: version
run: |
version=$(jq -r .version package.json)
if [ -z "$version" ] || [ "$version" = "null" ]; then
echo "::error::No version found in package.json"
exit 1
fi
echo "version=$version" >> "$GITHUB_OUTPUT"

- name: Fail if release already exists
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.version.outputs.version }}
run: |
if gh release view "$VERSION" --repo "$GITHUB_REPOSITORY" \
Comment thread
delthas marked this conversation as resolved.
>/dev/null 2>&1; then
echo "::error::Release $VERSION already exists"
exit 1
fi

- name: Fail if tag already exists
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
if git rev-parse -q --verify "refs/tags/$VERSION" >/dev/null; then
echo "::error::Tag $VERSION already exists (possibly on a" \
"different commit); refusing to move it"
exit 1
fi

- name: Create Release
uses: softprops/action-gh-release@v3
env:
GITHUB_TOKEN: ${{ github.token }}
with:
name: ${{ steps.version.outputs.version }}
tag_name: ${{ steps.version.outputs.version }}
generate_release_notes: true
target_commitish: ${{ github.sha }}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
"scripts": {
"build": "tsc",
"build_doc": "cd documentation/listingAlgos/pics; dot -Tsvg delimiterStateChart.dot > delimiterStateChart.svg; dot -Tsvg delimiterMasterV0StateChart.dot > delimiterMasterV0StateChart.svg; dot -Tsvg delimiterVersionsStateChart.dot > delimiterVersionsStateChart.svg",
"coverage": "export NODE_OPTIONS=\"--tls-max-v1.2\" && nyc --clean jest tests --coverage --testTimeout=120000 --forceExit --testPathIgnorePatterns tests/functional/pykmip",
"coverage": "export NODE_OPTIONS=\"--tls-max-v1.2\" && nyc --clean jest tests --coverage --testTimeout=120000 --forceExit --testPathIgnorePatterns 'tests/functional/pykmip|tests/workflows'",
"dev": "nodemon --ext ts --ignore build --exec \"yarn build\"",
"ft_pykmip_test": "jest tests/functional/pykmip",
"ft_test": "jest tests/functional --testTimeout=120000 --forceExit --testPathIgnorePatterns tests/functional/pykmip",
Expand Down
3 changes: 3 additions & 0 deletions tests/workflows/fixtures/package-no-version.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "arsenal"
}
4 changes: 4 additions & 0 deletions tests/workflows/fixtures/package-versioned.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "arsenal",
"version": "8.3.12"
}
15 changes: 15 additions & 0 deletions tests/workflows/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Config } from '@jest/types';

const jestConfig: Config.InitialOptions = {
verbose: true,
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
clearMocks: true,
resetMocks: true,
moduleDirectories: ['node_modules', '<rootDir>/node_modules'],
maxWorkers: 1,
testTimeout: 120000,
};

export default jestConfig;
23 changes: 23 additions & 0 deletions tests/workflows/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "arsenal-workflow-tests",
"version": "1.0.0",
"description": "Tests for arsenal GitHub Actions workflows",
"private": true,
"license": "Apache-2.0",
"scripts": {
"test": "jest"
},
"devDependencies": {
"@kie/act-js": "^2.6.2",
"@kie/mock-github": "^2.0.2",
"@types/jest": "^30.0.0",
"@types/node": "^24.13.2",
"jest": "^30.4.2",
"ts-jest": "^29.4.11",
"ts-node": "^10.9.2",
"typescript": "^6.0.3"
},
"resolutions": {
"tar": "^7.5.19"
}
}
92 changes: 92 additions & 0 deletions tests/workflows/release.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Act } from "@kie/act-js";
import { MockGithub } from "@kie/mock-github";
import path from "path";
import { exec as execCb } from "node:child_process";
import { promisify } from "node:util";

const exec = promisify(execCb);
const IMAGE = "ghcr.io/catthehacker/ubuntu:act-latest";

let github: MockGithub;
let act: Act;

async function setupRepo(versionFixture: string, branch: string) {
github = new MockGithub({
repo: {
arsenal: {
currentBranch: branch,
files: [
{ src: path.resolve(__dirname, "../..", ".github"), dest: ".github" },
{ src: path.resolve(__dirname, "fixtures", versionFixture), dest: "package.json" },
],
},
},
});
await github.setup();
act = new Act(github.repo.getPath("arsenal"));
act.setWorkflowFile(".github/workflows/release.yaml");
act.setPlatforms("ubuntu-latest", IMAGE);
act.setEnv("GITHUB_REPOSITORY", "scality/Arsenal");
act.setEnv("GITHUB_REF", `refs/heads/${branch}`);
}

async function createTag(tag: string) {
await exec(`git -C ${github.repo.getPath("arsenal")} tag --no-sign -m test ${tag}`);
}

// Stub the steps that need network / external services (git remote, the gh CLI,
// the release action); the guard logic under test stays real.
const mockSteps = {
release: [
{ name: "Fetch tags", mockWith: "echo skip-fetch-tags" },
{ name: "Fail if release already exists", mockWith: "echo no-release-found" },
{ name: "Create Release", mockWith: "echo skip-create-release" },
],
};

function run() {
return act.runEvent("workflow_dispatch", { mockSteps });
}

function step(result: { name: string; status: number }[], name: string) {
return result.find(r => r.name === `Main ${name}`);
}

afterEach(async () => {
await github.teardown();
});

test("rejects a release dispatched from a disallowed branch", async () => {
await setupRepo("package-versioned.json", "feature/not-a-release-branch");
const result = await run();
expect(step(result, "Reject disallowed branch")?.status).toBe(1);
});

test("allows a development/* branch and runs through to the release step", async () => {
await setupRepo("package-versioned.json", "development/8.3");
const result = await run();
// the branch guard is `if`-gated, so it is skipped (absent) on an allowed branch
expect(step(result, "Reject disallowed branch")).toBeUndefined();
expect(result.every(r => r.status === 0)).toBe(true);
expect(step(result, "Create Release")?.status).toBe(0);
});

test("allows a hotfix/* branch", async () => {
await setupRepo("package-versioned.json", "hotfix/8.3.1");
const result = await run();
expect(step(result, "Reject disallowed branch")).toBeUndefined();
expect(step(result, "Create Release")?.status).toBe(0);
});

test("fails when package.json has no version", async () => {
await setupRepo("package-no-version.json", "development/8.3");
const result = await run();
expect(step(result, "Read version from package.json")?.status).toBe(1);
});

test("fails when the tag already exists", async () => {
await setupRepo("package-versioned.json", "development/8.3");
await createTag("8.3.12");
const result = await run();
expect(step(result, "Fail if tag already exists")?.status).toBe(1);
});
14 changes: 14 additions & 0 deletions tests/workflows/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2019",
"module": "commonjs",
"strict": true,
"noImplicitAny": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"noEmit": true,
"typeRoots": ["./node_modules/@types"],
"types": ["jest", "node"]
},
"exclude": ["./node_modules"]
}
Loading
Loading