semantic-release-steam 2.1.3 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,6 +43,10 @@ Set these variables in your release environment:
43
43
  - `STEAM_USERNAME`
44
44
  - `STEAM_CONFIG_VDF` or `STEAM_CONFIG_VDF_B64`
45
45
  - `STEAMCMD_PATH` if SteamCMD isn't at `~/steamcmd/steamcmd.sh`
46
+ - `STEAM_PASSWORD` or `STEAM_REFRESH_TOKEN`, only if any mod sets `tags` - these set Steam
47
+ Workshop browse tags over a separate login, since SteamCMD itself has no way to set them.
48
+ `STEAM_REFRESH_TOKEN` skips the Steam Guard prompt; `STEAM_PASSWORD` needs one approval tap
49
+ per publish.
46
50
 
47
51
  SteamCMD must have a saved login in its `config/config.vdf`. See the [SteamCMD Workshop upload guide](https://partner.steamgames.com/doc/features/workshop/implementation#SteamCmdIntegration) for the item setup and VDF format.
48
52
 
@@ -57,6 +61,121 @@ The plugin updates Workshop items. Create each item before your first release, t
57
61
  - [GitHub Actions setup](https://semantic-release.gitbook.io/semantic-release/recipes/ci-configurations/github-actions)
58
62
  - [Steam Workshop implementation guide](https://partner.steamgames.com/doc/features/workshop/implementation)
59
63
 
64
+ ## Recovering a missed tag update
65
+
66
+ If a publish uploads through SteamCMD but fails before the tag step finishes (a missed Steam
67
+ Guard tap, a dropped connection), retag the item without a new release:
68
+
69
+ ```bash
70
+ npx semantic-release-steam-apply-tags --config release.config.mjs.json --target main
71
+ ```
72
+
73
+ `--config` points at a JSON file holding just this plugin's own options (`appId`,
74
+ `branchTargets`, `mods`), not the full `release.config.mjs`. Write it once, for example with
75
+ `node -e "console.log(JSON.stringify(require('./release.config.mjs').default.plugins[0][1]))" > release.config.mjs.json`.
76
+
77
+ `--target` is the git branch name, resolved through the same `branchTargets` map as a normal
78
+ publish.
79
+
80
+ ## Running these by hand from GitHub Actions
81
+
82
+ Add `workflow_dispatch` to the workflow that runs `semantic-release`, alongside its existing
83
+ trigger:
84
+
85
+ ```yaml
86
+ on:
87
+ push:
88
+ branches: [main]
89
+ workflow_dispatch: {}
90
+ ```
91
+
92
+ `semantic-release` still decides whether a release is needed. A manual run with no new
93
+ commits logs "no release published" and exits clean.
94
+
95
+ Tag recovery needs its own workflow, since it takes a branch name as input instead of reading
96
+ the one that triggered the run:
97
+
98
+ ```yaml
99
+ name: apply-tags
100
+
101
+ on:
102
+ workflow_dispatch:
103
+ inputs:
104
+ target:
105
+ description: Branch name, resolved through branchTargets like a normal publish
106
+ required: true
107
+
108
+ jobs:
109
+ apply-tags:
110
+ runs-on: ubuntu-latest
111
+ steps:
112
+ - uses: actions/checkout@v7
113
+ - uses: actions/setup-node@v7
114
+ with:
115
+ node-version: '22'
116
+ - run: npm ci
117
+ - run: npx semantic-release-steam-apply-tags --config release.config.mjs.json --target "${{ github.event.inputs.target }}"
118
+ env:
119
+ STEAM_USERNAME: ${{ secrets.STEAM_USERNAME }}
120
+ STEAM_CONFIG_VDF_B64: ${{ secrets.STEAM_CONFIG_VDF_B64 }}
121
+ STEAM_PASSWORD: ${{ secrets.STEAM_PASSWORD }}
122
+ ```
123
+
124
+ Run it from the repo's Actions tab and pick the branch. It retags the item without touching
125
+ SteamCMD or cutting a release.
126
+
127
+ ## Forcing a release
128
+
129
+ `semantic-release` reads commit messages to decide the version bump. It has no flag for
130
+ forcing one. To force a release on demand, push an empty commit with a conventional-commit
131
+ message, then run `semantic-release` in the same job:
132
+
133
+ ```yaml
134
+ name: force-release
135
+
136
+ on:
137
+ workflow_dispatch:
138
+ inputs:
139
+ bump:
140
+ description: Release type to force
141
+ required: true
142
+ type: choice
143
+ options: [patch, minor, major]
144
+ message:
145
+ description: Commit message, written into the changelog
146
+ required: true
147
+
148
+ jobs:
149
+ force-release:
150
+ runs-on: ubuntu-latest
151
+ steps:
152
+ - uses: actions/checkout@v7
153
+ - uses: actions/setup-node@v7
154
+ with:
155
+ node-version: '22'
156
+ - run: npm ci
157
+ - run: |
158
+ git config user.name "github-actions[bot]"
159
+ git config user.email "github-actions[bot]@users.noreply.github.com"
160
+ case "${{ inputs.bump }}" in
161
+ patch) type=fix ;;
162
+ minor) type=feat ;;
163
+ major) type='feat!' ;;
164
+ esac
165
+ git commit --allow-empty -m "$type: ${{ inputs.message }}"
166
+ git push
167
+ - run: npx semantic-release
168
+ env:
169
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
170
+ STEAM_USERNAME: ${{ secrets.STEAM_USERNAME }}
171
+ STEAM_CONFIG_VDF_B64: ${{ secrets.STEAM_CONFIG_VDF_B64 }}
172
+ STEAM_PASSWORD: ${{ secrets.STEAM_PASSWORD }}
173
+ ```
174
+
175
+ Run `semantic-release` in this same job instead of relying on the push above to trigger your
176
+ normal release workflow. A push made with the default `GITHUB_TOKEN` doesn't trigger other
177
+ workflows, so the push-based workflow would never run.
178
+
60
179
  ## License
61
180
 
62
181
  [MIT](./LICENSE)
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from 'node:fs/promises';
3
+ import { realpathSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { verifySteamPublishConfig } from '../lib/config.mjs';
6
+ import { applyWorkshopTags } from '../lib/workshop-tags.mjs';
7
+
8
+ function parseArgs(argv) {
9
+ const args = { config: null, target: null };
10
+ for (let i = 0; i < argv.length; i += 1) {
11
+ if (argv[i] === '--config') args.config = argv[i + 1];
12
+ if (argv[i] === '--target') args.target = argv[i + 1];
13
+ }
14
+ return args;
15
+ }
16
+
17
+ function resolveTags(mod, target) {
18
+ return mod.metadata?.[target]?.tags ?? mod.tags;
19
+ }
20
+
21
+ export async function run({
22
+ argv = process.argv.slice(2),
23
+ env = process.env,
24
+ logger = console,
25
+ readConfig = path => readFile(path, 'utf8'),
26
+ verifyConfig = verifySteamPublishConfig,
27
+ applyTags = applyWorkshopTags,
28
+ } = {}) {
29
+ const { config: configPath, target: branchName } = parseArgs(argv);
30
+ if (!configPath || !branchName) {
31
+ throw new Error('usage: semantic-release-steam-apply-tags --config <file> --target <branch>');
32
+ }
33
+
34
+ const pluginConfig = JSON.parse(await readConfig(configPath));
35
+
36
+ const hasBranchTargets = pluginConfig.branchTargets && typeof pluginConfig.branchTargets === 'object'
37
+ && !Array.isArray(pluginConfig.branchTargets);
38
+ const hasMods = Array.isArray(pluginConfig.mods);
39
+ if (!hasBranchTargets || !hasMods) {
40
+ throw new Error(
41
+ `Config file "${configPath}" must contain "branchTargets" (an object) and "mods" (an array) - ` +
42
+ 'the same options you pass to the semantic-release-steam plugin, not the full release.config.mjs. ' +
43
+ `Found top-level keys: [${Object.keys(pluginConfig).join(', ')}].`,
44
+ );
45
+ }
46
+
47
+ const state = await verifyConfig({
48
+ env,
49
+ branchName,
50
+ branchTargets: pluginConfig.branchTargets,
51
+ mods: pluginConfig.mods,
52
+ appId: pluginConfig.appId,
53
+ });
54
+
55
+ if (!state.shouldPublish) {
56
+ throw new Error(`Branch "${branchName}" has no configured target in branchTargets.`);
57
+ }
58
+
59
+ const mods = state.mods.map(mod => ({
60
+ name: mod.name,
61
+ publishedFileId: mod.workshopIds[state.target],
62
+ tags: resolveTags(mod, state.target),
63
+ }));
64
+
65
+ await applyTags({
66
+ steamUsername: env.STEAM_USERNAME,
67
+ steamPassword: env.STEAM_PASSWORD,
68
+ steamRefreshToken: env.STEAM_REFRESH_TOKEN,
69
+ appId: pluginConfig.appId,
70
+ mods,
71
+ logger,
72
+ });
73
+ }
74
+
75
+ function isMainModule() {
76
+ if (!process.argv[1]) return false;
77
+ try {
78
+ return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
79
+ } catch {
80
+ return false;
81
+ }
82
+ }
83
+
84
+ if (isMainModule()) {
85
+ run().catch(error => {
86
+ console.error(error.message);
87
+ process.exitCode = 1;
88
+ });
89
+ }
package/index.mjs CHANGED
@@ -4,6 +4,7 @@ import { buildSteamDescription } from './lib/description.mjs';
4
4
  import { compileReadme, writeCompiledReadme } from './lib/readme.mjs';
5
5
  import { stageModContent } from './lib/stage-content.mjs';
6
6
  import { uploadWorkshopItem } from './lib/steamcmd.mjs';
7
+ import { applyWorkshopTags } from './lib/workshop-tags.mjs';
7
8
 
8
9
  function isDryRun(context) {
9
10
  return Boolean(context?.options?.dryRun);
@@ -27,6 +28,7 @@ function resolveOverrides(context) {
27
28
  uploadItem: context.uploadWorkshopItem ?? uploadWorkshopItem,
28
29
  compile: context.compileReadme ?? compileReadme,
29
30
  writeCompiled: context.writeCompiledReadme ?? writeCompiledReadme,
31
+ applyTags: context.applyWorkshopTags ?? applyWorkshopTags,
30
32
  };
31
33
  }
32
34
 
@@ -83,6 +85,8 @@ export async function publish(pluginConfig, context) {
83
85
  ? pluginConfig.assetBaseUrlTemplate.replace('{branch}', context.branch.name)
84
86
  : '';
85
87
 
88
+ const taggableMods = [];
89
+
86
90
  for (const mod of state.mods) {
87
91
  const modPath = resolve(cwd, mod.path);
88
92
  const markdown = await compileModReadme(pluginConfig, modPath, overrides);
@@ -117,6 +121,7 @@ export async function publish(pluginConfig, context) {
117
121
  steamCmdPath: context.env.STEAMCMD_PATH ?? '~/steamcmd/steamcmd.sh',
118
122
  steamUsername: context.env.STEAM_USERNAME,
119
123
  steamConfigPath: state.steamConfigPath,
124
+ steamHome: state.steamHome,
120
125
  appId: pluginConfig.appId,
121
126
  stagePath,
122
127
  publishedFileId,
@@ -132,6 +137,18 @@ export async function publish(pluginConfig, context) {
132
137
  });
133
138
 
134
139
  context.logger.log(`Published ${mod.name} to ${state.target} workshop item ${publishedFileId}`);
140
+ taggableMods.push({ name: mod.name, publishedFileId, tags: metadata.tags });
141
+ }
142
+
143
+ if (!dryRun) {
144
+ await overrides.applyTags({
145
+ steamUsername: context.env.STEAM_USERNAME,
146
+ steamPassword: context.env.STEAM_PASSWORD,
147
+ steamRefreshToken: context.env.STEAM_REFRESH_TOKEN,
148
+ appId: pluginConfig.appId,
149
+ mods: taggableMods,
150
+ logger: context.logger,
151
+ });
135
152
  }
136
153
 
137
154
  return undefined;
package/lib/config.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { mkdir, writeFile } from 'node:fs/promises';
1
+ import { mkdir, writeFile, symlink } from 'node:fs/promises';
2
2
  import { tmpdir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import { randomBytes } from 'node:crypto';
@@ -49,11 +49,15 @@ export async function resolveSteamConfigPath(env) {
49
49
 
50
50
  if (env.STEAM_CONFIG_VDF_B64 && env.STEAM_CONFIG_VDF_B64.trim().length > 0) {
51
51
  const buffer = Buffer.from(env.STEAM_CONFIG_VDF_B64, 'base64');
52
- const dir = join(tmpdir(), `semantic-release-steam-${randomBytes(6).toString('hex')}`);
52
+ const home = join(tmpdir(), `semantic-release-steam-${randomBytes(6).toString('hex')}`);
53
+ const steamDir = join(home, '.steam');
54
+ const dir = join(steamDir, 'config');
53
55
  await mkdir(dir, { recursive: true });
56
+ await symlink(steamDir, join(steamDir, 'root'));
57
+ await symlink(steamDir, join(steamDir, 'steam'));
54
58
  const path = join(dir, 'config.vdf');
55
59
  await writeFile(path, buffer);
56
- return path;
60
+ return { path, home };
57
61
  }
58
62
 
59
63
  return null;
@@ -81,13 +85,16 @@ export async function verifySteamPublishConfig({
81
85
  );
82
86
  }
83
87
 
84
- const steamConfigPath = await resolveSteamConfigPath(env);
85
- if (!steamConfigPath) {
88
+ const resolvedConfig = await resolveSteamConfigPath(env);
89
+ if (!resolvedConfig) {
86
90
  throw new Error(
87
91
  'Steam config.vdf path is required for Steam publishing. ' +
88
92
  'Set STEAM_CONFIG_VDF=/path/to/config.vdf, or STEAM_CONFIG_VDF_B64=<base64 of config.vdf contents>.',
89
93
  );
90
94
  }
95
+ const steamConfig = typeof resolvedConfig === 'string'
96
+ ? { path: resolvedConfig, home: null }
97
+ : resolvedConfig;
91
98
 
92
99
  if (!Array.isArray(mods) || mods.length === 0) {
93
100
  throw new Error('mods is required and must contain at least one entry');
@@ -132,5 +139,21 @@ export async function verifySteamPublishConfig({
132
139
  );
133
140
  }
134
141
 
135
- return { shouldPublish: true, target, mods: publishable, steamConfigPath };
142
+ const needsTagCredentials = publishable.some(
143
+ mod => (mod.metadata?.[target]?.tags ?? mod.tags)?.length > 0,
144
+ );
145
+ if (needsTagCredentials && !env.STEAM_PASSWORD && !env.STEAM_REFRESH_TOKEN) {
146
+ throw new Error(
147
+ 'STEAM_PASSWORD or STEAM_REFRESH_TOKEN is required when a mod configures tags. ' +
148
+ 'Set one of them, or remove tags to skip the Workshop tags step.',
149
+ );
150
+ }
151
+
152
+ return {
153
+ shouldPublish: true,
154
+ target,
155
+ mods: publishable,
156
+ steamConfigPath: steamConfig.path,
157
+ steamHome: steamConfig.home,
158
+ };
136
159
  }
package/lib/steamcmd.mjs CHANGED
@@ -39,6 +39,7 @@ export async function uploadWorkshopItem({
39
39
  steamCmdPath,
40
40
  steamUsername,
41
41
  steamConfigPath,
42
+ steamHome,
42
43
  stagePath,
43
44
  publishedFileId,
44
45
  changenote,
@@ -82,6 +83,7 @@ export async function uploadWorkshopItem({
82
83
  env: {
83
84
  ...process.env,
84
85
  STEAM_CONFIG_VDF: steamConfigPath,
86
+ ...(steamHome ? { HOME: steamHome } : {}),
85
87
  },
86
88
  });
87
89
 
@@ -0,0 +1,195 @@
1
+ const LOGIN_TIMEOUT_MS = 120_000;
2
+ const CONNECT_TIMEOUT_MS = 30_000;
3
+ const UPDATE_TIMEOUT_MS = 30_000;
4
+ const LOGOFF_TIMEOUT_MS = 5_000;
5
+ const VERIFY_ATTEMPTS = 3;
6
+ const VERIFY_RETRY_DELAY_MS = 2_000;
7
+
8
+ const GUARD_TYPE_DEVICE_CONFIRMATION = 4;
9
+ const GUARD_TYPE_EMAIL_CONFIRMATION = 5;
10
+
11
+ export function canPollForGuardApproval(validActions) {
12
+ return (validActions ?? []).some(
13
+ action => action.type === GUARD_TYPE_DEVICE_CONFIRMATION || action.type === GUARD_TYPE_EMAIL_CONFIRMATION,
14
+ );
15
+ }
16
+
17
+ async function loginWithPassword({ steamUsername, steamPassword, logger }) {
18
+ const { LoginSession, EAuthTokenPlatformType } = await import('steam-session');
19
+ const session = new LoginSession(EAuthTokenPlatformType.SteamClient);
20
+ session.loginTimeout = LOGIN_TIMEOUT_MS;
21
+
22
+ const startResult = await session.startWithCredentials({
23
+ accountName: steamUsername,
24
+ password: steamPassword,
25
+ });
26
+
27
+ if (startResult.actionRequired) {
28
+ if (!canPollForGuardApproval(startResult.validActions)) {
29
+ throw new Error(
30
+ 'Steam Guard on this account needs an email or mobile authenticator code, which STEAM_PASSWORD ' +
31
+ 'cannot approve automatically. Use STEAM_REFRESH_TOKEN instead, or enable app-based confirmation.',
32
+ );
33
+ }
34
+ logger.log('Approve the Steam Guard prompt on your phone to publish Workshop tags.');
35
+ }
36
+
37
+ await /** @type {Promise<void>} */ (new Promise((resolve, reject) => {
38
+ session.once('authenticated', () => resolve());
39
+ session.once('timeout', () => reject(new Error('Steam Guard approval timed out')));
40
+ session.once('error', reject);
41
+ }));
42
+
43
+ return session.refreshToken;
44
+ }
45
+
46
+ export function connectWithTimeout(user, timeoutMs, trigger) {
47
+ return new Promise((resolve, reject) => {
48
+ const settle = (fn, value) => {
49
+ clearTimeout(timer);
50
+ user.removeAllListeners('loggedOn');
51
+ user.removeAllListeners('error');
52
+ fn(value);
53
+ };
54
+ const timer = setTimeout(() => {
55
+ user.logOff();
56
+ settle(reject, new Error('Steam CM connection timed out'));
57
+ }, timeoutMs);
58
+ user.once('loggedOn', () => settle(resolve));
59
+ user.once('error', err => settle(reject, err));
60
+ trigger();
61
+ });
62
+ }
63
+
64
+ async function connectToCM(refreshToken) {
65
+ const SteamUser = (await import('steam-user')).default;
66
+ const user = new SteamUser({ autoRelogin: false });
67
+
68
+ await connectWithTimeout(user, CONNECT_TIMEOUT_MS, () => user.logOn({ refreshToken }));
69
+
70
+ const died = new Promise((_resolve, reject) => {
71
+ user.on('error', reject);
72
+ user.on('disconnected', eresult => reject(new Error(`Steam CM session disconnected (eresult ${eresult})`)));
73
+ });
74
+
75
+ return { user, died };
76
+ }
77
+
78
+ async function sendTagUpdate(user, { appid, publishedfileid, tags }) {
79
+ const SteamUser = (await import('steam-user')).default;
80
+ const Schema = (await import('steam-user/protobufs/generated/_load.js')).default;
81
+
82
+ return /** @type {Promise<void>} */ (new Promise((resolve, reject) => {
83
+ const body = Schema.CPublishedFile_Update_Request.encode({ appid, publishedfileid, tags }).finish();
84
+ const timer = setTimeout(() => reject(new Error('PublishedFile.Update#1 timed out')), UPDATE_TIMEOUT_MS);
85
+
86
+ user._send(
87
+ { msg: SteamUser.EMsg.ServiceMethodCallFromClient, proto: { target_job_name: 'PublishedFile.Update#1' } },
88
+ Buffer.from(body),
89
+ (_body, header) => {
90
+ clearTimeout(timer);
91
+ const eresult = header?.proto?.eresult;
92
+ if (eresult !== SteamUser.EResult.OK) {
93
+ reject(new Error(`PublishedFile.Update#1 for ${publishedfileid} returned eresult ${eresult}`));
94
+ return;
95
+ }
96
+ resolve();
97
+ },
98
+ );
99
+ }));
100
+ }
101
+
102
+ function fetchPublishedTags(user, publishedfileid, expectedTags) {
103
+ return new Promise((resolve, reject) => {
104
+ user.getPublishedFileDetails([publishedfileid], (err, results) => {
105
+ if (err) {
106
+ reject(err);
107
+ return;
108
+ }
109
+ const actual = (results[publishedfileid]?.tags ?? []).map(t => (t.tag ?? t).toLowerCase()).sort();
110
+ const expected = expectedTags.map(t => t.toLowerCase()).sort();
111
+ resolve({ matches: JSON.stringify(actual) === JSON.stringify(expected), actual });
112
+ });
113
+ });
114
+ }
115
+
116
+ export async function verifyPublishedTags(
117
+ user,
118
+ publishedfileid,
119
+ expectedTags,
120
+ attempts = VERIFY_ATTEMPTS,
121
+ delayMs = VERIFY_RETRY_DELAY_MS,
122
+ ) {
123
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
124
+ const result = await fetchPublishedTags(user, publishedfileid, expectedTags);
125
+ if (result.matches || attempt === attempts) {
126
+ return result;
127
+ }
128
+ await new Promise(resolve => setTimeout(resolve, delayMs));
129
+ }
130
+ return undefined;
131
+ }
132
+
133
+ export function logOffAndWait(user, timeoutMs = LOGOFF_TIMEOUT_MS) {
134
+ return /** @type {Promise<void>} */ (new Promise(resolve => {
135
+ if (!user.steamID) {
136
+ resolve();
137
+ return;
138
+ }
139
+ const timer = setTimeout(resolve, timeoutMs);
140
+ user.once('disconnected', () => { clearTimeout(timer); resolve(); });
141
+ user.logOff();
142
+ }));
143
+ }
144
+
145
+ export async function applyWorkshopTags({
146
+ steamUsername,
147
+ steamPassword,
148
+ steamRefreshToken,
149
+ appId,
150
+ mods,
151
+ logger,
152
+ login = loginWithPassword,
153
+ connect = connectToCM,
154
+ disconnect = logOffAndWait,
155
+ sendUpdate = sendTagUpdate,
156
+ verify = verifyPublishedTags,
157
+ }) {
158
+ const taggedMods = mods.filter(mod => Array.isArray(mod.tags) && mod.tags.length > 0);
159
+ if (taggedMods.length === 0) {
160
+ return;
161
+ }
162
+
163
+ const refreshToken = steamRefreshToken || await login({ steamUsername, steamPassword, logger });
164
+ const { user, died } = await connect(refreshToken);
165
+
166
+ const tagged = [];
167
+ try {
168
+ for (const mod of taggedMods) {
169
+ await Promise.race([
170
+ sendUpdate(user, { appid: appId, publishedfileid: mod.publishedFileId, tags: mod.tags }),
171
+ died,
172
+ ]);
173
+
174
+ const result = await Promise.race([verify(user, mod.publishedFileId, mod.tags), died]);
175
+ if (!result.matches) {
176
+ throw new Error(
177
+ `Workshop tags for "${mod.name}" did not verify after the update. ` +
178
+ `Sent [${mod.tags.join(', ')}], Steam now reports [${result.actual.join(', ')}].`,
179
+ );
180
+ }
181
+
182
+ tagged.push(mod.name);
183
+ }
184
+ } catch (error) {
185
+ const untagged = taggedMods.slice(tagged.length).map(mod => mod.name);
186
+ const message = error instanceof Error ? error.message : String(error);
187
+ throw new Error(
188
+ `Workshop tag update failed: ${message} Tagged: [${tagged.join(', ')}]. ` +
189
+ `Still untagged on Workshop: [${untagged.join(', ')}].`,
190
+ { cause: error },
191
+ );
192
+ } finally {
193
+ await disconnect(user);
194
+ }
195
+ }
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "semantic-release-steam",
3
- "version": "2.1.3",
3
+ "version": "2.2.0",
4
4
  "description": "semantic-release plugin that publishes a Steam Workshop item from a built mod directory.",
5
5
  "type": "module",
6
6
  "main": "index.mjs",
7
7
  "types": "./index.d.ts",
8
+ "bin": {
9
+ "semantic-release-steam-apply-tags": "./bin/apply-tags.mjs"
10
+ },
8
11
  "exports": {
9
12
  ".": {
10
13
  "types": "./index.d.ts",
@@ -16,6 +19,7 @@
16
19
  "files": [
17
20
  "index.mjs",
18
21
  "index.d.ts",
22
+ "bin/",
19
23
  "lib/",
20
24
  "schema/",
21
25
  "LICENSE",
@@ -62,14 +66,16 @@
62
66
  }
63
67
  },
64
68
  "dependencies": {
65
- "@steamdown/core": "^1.0.0-beta.2"
69
+ "@steamdown/core": "^1.0.0-beta.2",
70
+ "steam-session": "^1.9.4",
71
+ "steam-user": "^5.3.0"
66
72
  },
67
73
  "devDependencies": {
68
- "@eslint/js": "^9.0.0",
69
- "@types/node": "^20.0.0",
70
- "eslint": "^9.0.0",
71
- "globals": "^15.0.0",
74
+ "@eslint/js": "^10.0.1",
75
+ "@types/node": "^26.6.1",
76
+ "eslint": "^10.10.0",
77
+ "globals": "^17.12.0",
72
78
  "semantic-release": "^25.0.0",
73
- "typescript": "^5.5.0"
79
+ "typescript": "^7.0.2"
74
80
  }
75
81
  }